2011년 1월 17일 월요일

Spring에서 JUnit 테스트 시 Transaction 처리

http://viralpatel.net/blogs/2010/11/spring3-mvc-hibernate-maven-tutorial-eclipse-example.html

위의 블로그에 방문하면 spring3 기반에서 hibernate를 이용해서 dao를 개발하는 방법을 비교적 친절하고 상세하게 잘 정리해 두었다. 참고하기 바란다.

이번 포스트에서 이야기하고자 하는 내용은 DAO 컴포넌트를 JUnit으로 테스트 할 때 Transaction 처리에 관한 내용이다.

위의 블로그는 WEB-INF 폴더에 있는 spring-servlet.xml 파일과 jdbc.properties 파일을 조금 수정해 주어야 한다.

위의 두 파일을 src/main/resources 소스 폴더에 spring 이란 패키지를 만들어서 복사한다.
spring-servlet.xml 파일에서 jdbc.properties 파일을 참조하는 부분을 아래와 같이 수정한다.

<bean id="propertyConfigurer" 
      class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
      p:location="spring/jdbc.properties">
</bean> 

이제서야 JUnit TestCase를 작성할 사전 준비가 완료되었다.

package net.viralpatel.contact.dao;

import java.util.List;

import junit.framework.Assert;

import net.viralpatel.contact.form.Contact;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath*:spring/spring-servlet.xml"})
public class ContactDAOTest {

  @Autowired
  private ContactDAO contactDAO;
  
  @Test
  @Transactional
  public void testListContact() throws Exception {
    List contactList = contactDAO.listContact();
    
    Assert.assertNotNull(contactList);
  }
}


위의 소스를 살펴보자.
@RunWith(SpringJUnit4ClassRunner.class)
@RunWith Annotation을 사용해야 JUnit에서 Spring을 설정을 그대로 사용할 수 있다. 테스트케이스에서 스프링에서 설정한 Bean을 Injection 받아서 사용할 수 있다는 의미다.
그런데 SpringJUnit4ClassRunner.class 사용시 주의해야 할 사항이 있다.
Spring 2.5.x 버전과 Spring 3.0.x 에서의 상속받는 상위 클래스가 다르다.
2.5.x 버전의 SpringJUnit4ClassRunner는 org.junit.internal.runners.JUnit4ClassRunner 클래스를 상속받아 구현되었는데 이는 JUnit4.3 이상에서 제공하는 클래스다.
3.0.x 버전의 SpringJUnit4ClassRunner는 org.junit.runners.BlockJUnit4ClassRunner 클래스를 상속받아 구현되었는데 이는 JUnit4.5 이상에서 제공하는 클래스다.
따라서 spring 버전에 따라서 사용해야하는 junit 버전이 달라짐을 유의해야 한다.

@ContextConfiguration(locations={"classpath*:spring/spring-servlet.xml"})
WEB-INF/spring-servlet.xml 파일을 리소스 폴더로 옮기고 그 클래스패스를 설정해 주었다.

먼저 DAO 구현 클래스를 살펴보자.
package net.viralpatel.contact.dao;


import java.util.List;

import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import net.viralpatel.contact.form.Contact;

@Repository()
public class ContactDAOImpl implements ContactDAO {

  @Autowired
  private SessionFactory sessionFactory;
  
  
  public void addContact(Contact contact) {
    sessionFactory.getCurrentSession().save(contact);
  }

  @SuppressWarnings("unchecked")
  public List listContact() {
    return sessionFactory.getCurrentSession().createQuery("from Contact").list();
  }

  public void removeContact(Integer id) {
    Contact contact = (Contact) sessionFactory.getCurrentSession().load(Contact.class, id);
    
    if(contact != null) {
      sessionFactory.getCurrentSession().delete(contact);
    }
  }
}


소스코드를 살펴보면 "sessionFactory.getCurrentSession()" 부분이 있다.
sessionFactory는 Hibernate의 세션을 관리하는 팩토리로
getCurrentSession()은 트랜잭션을 가지고 있는 현재 세션을 리턴한다.
따라서 트랜잭션에 없는 상황에서는 "No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here"라는 에러가 발생한다.
위의 ContactDAOTest에서 @Transactional annotation을 제거하고 실행하면 위와 같은 에러가 발생하는 것을 확인할 수 있다.

일반적인 경우라면 DAO에 Transactional annotation을 붙이지는 않는다.
보통 DAO를 사용하고 있는 서비스에 붙히는데, 전에는 서비스 인터페이스에 설정하는 걸 즐겨했다. 하지만 인터페이스에 @Transactional을 설정하는 것보다 구현 클래스에 설정하는게 더 적합하다. 인터페이스는 Transaction 설정과 같은 세부사항에 관여하면 안 되기 때문이다.

어쨋거나 이 글에서 이야기하고자 하는 내용은 지금부터다.

JUnit의 @Test annotation을 설정하여 DAO에 대한 테스트를 실행하는 경우 Insert, Update, Delete를 테스트할때마다 매번 DB에 반영되는게 번거롭고 문제도 가끔씩 발생시킨다.

테스트 자동화시에는 대부분의 경우 이렇게 데이터에 영향을 미치는 연산을 제외시키기 위해서 @Ignore annotation을 테스트 메서드에 달아 두곤 했다.

하지만 스프링에서 @Test annotation과 함께 설정된 @Transactional은 항상 rollback된다.
따라서 create, update, delete의 로직이 정상적으로 수행됨은 확인하면서도 실제로 DB에 반영되지는 않는다. 롤백이 되는 것과 에러가 발생하는것은 분명한 차이가 있다. PK가 중복된다면 rollback이 아니라 에러가 발생한 경우다. 데이터베이스에 데이터가 없다고 정상처리되지 않았다고 판단하면 안된다. JUnit이 초록색 불을 밝히는 것과 붉은색 불을 밝히는 것은 엄청난 차이가 있다. JUnit이 초록불이라면 데이터베이스에 값이 반영되지 않는다 하더라도 대부분의 경우에는 믿어도 된다.
매번 실행한다고 해도 데이터에 영향이 미치지 않는다.

하지만 눈으로 직접 추가, 변경, 삭제를 확인해보고 싶다면 어떻게 해야 할까. JUnit의 @Test 메서드를 실행하면서도 트랜잭션이 롤백되지 않도록 설정하면 된다.
첫번째 방법은 org.springframework.test.annotation.Rollback annotation을 사용하면 된다.

@Test
  @Transactional
  @Rollback(false)
  public void testAddContact() throws Exception {
    Contact contact = new Contact();
    contact.setFirstname("YG");
    contact.setLastname("Kim");
    
    contactDAO.addContact(contact);
    
    Assert.assertNotSame(0, contact.getId());
  }

위와 같이 @Rollback(false)로 설정하면 테스트 메서드라 하더라도 롤백되지 않는다.

다른 방법으로는 테스트 클래스에 지정하는 방식이다.
org.springframework.test.context.transaction.TransactionConfiguration annotation을 사용한다.
@TransactionConfiguration(transactionManager="transactionManager", defaultRollback=false)
public class ContactDAOTest {
...
}

위와 같이 설정하면 모든 테스트 메서드의 롤백이 실행되지 않고 데이터베이스에 반영된다.
만약 위와 같이 설정하고 특정 메서드에서 롤백이 되기를 원한다면 @Rollback(true)로 지정하면 된다.

스프링의 테스팅을 상세히 살펴보고자 한다면 http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/testing.html 사이트에 방문하기를 권한다.

댓글 2개:

  1. 대단히 좋은 글입니다.
    감사합니다.

    답글삭제
  2. spring + mybatis 를
    간단히 junit 테스트 코드를 작성하는데
    많은 도움이 되었습니다. ㅎ
    감사합니다.

    답글삭제