본문 바로가기
Homo Faber/Idioms

Hibernate 테스팅 - Unitils 사용하기

by javauser 2008. 2. 18.
Hibernate의 최대 장점은 아마도 어떠한 DB를 사용하더라도 코드의 변화가 적다는 것이다. 따라서, 테스팅에서도 실제 DB를 사용하지 않더라도 코딩이 가능하다. 여기서는 Hibernate와 Spring을 사용해서 테스팅 하는 예제를 설명하겠다. (도메인 모델은 이전에 설명한 일대다 관계의 예제)

Spring Framework에서는 ORM과 관련된 Support 클래스를 제공하고 있는데, Hibernate 3에 대해서는 org.springframework.orm.hibernate3.support.HibernateDaoSupport 와 같은 지원 클래스를 제공한다.

우선, 데이터 리소스를 접근하는 인터페이스를 다음과 같이 정의할 수 있다.
public interface SaveRepository {
 boolean saveTopParent(TopParentObject tpo);
 TopParentObject getTopParent(String id);
}
인터페이스는 저장과 조회를 수행하는 두가지 오퍼레이션을 가지고 있다.
우선 이를 Mock 테스트를 사용하여 간단하게 테스트하면 다음과 같다.
public class SaveRepositoryTest {
 private Mockery context = new Mockery();
 private final SaveRepository saveRepo = context.mock(SaveRepository.class);
 
 @Test public void saveTopParent() throws Exception {
  context.checking(new Expectations() {{
   allowing (saveRepo).saveTopParent(with(aNull(TopParentObject.class))); will(returnValue(Boolean.FALSE));
   allowing (saveRepo).saveTopParent(with(any(TopParentObject.class))); will(returnValue(Boolean.TRUE));
  }});
 
  assertFalse(saveRepo.saveTopParent(null));
  TopParentObject tpo = new TopParentObject("id", "field");
  assertTrue(saveRepo.saveTopParent(tpo));
 }
 
 @Test public void getTopParent() throws Exception {
  final TopParentObject tpo = new TopParentObject("1", "bbb");
  context.checking(new Expectations() {{
   allowing (saveRepo).getTopParent(with(aNull(String.class))); will(returnValue(null));
   allowing (saveRepo).getTopParent(new String("1")); will(returnValue(tpo));
  }});
 
  assertNull(saveRepo.getTopParent(null));
  TopParentObject result = saveRepo.getTopParent(new String("1"));
  assertNotNull(result);
  assertEquals(tpo.getField(), result.getField());
 }
}
위의 코드는 JUnit4 오 Jmock 을 사용해서 작성되었다. saveTopParent() 오퍼레이션의 @Test는 JUnit4에서 지원하는 annotation을 사용한 것이다. SaveRepository 인터페이스는 Jmock의 Mockery 클래스를 통해 Mock 객체로 만들어진 상태이다.
Mock 객체의 saveTopParent() 메소드는 null을 인자로 받는 경우 false를 리턴하고, 그 외에는 true를 리턴하게끔 세팅되어 있다. 이를 테스트 실행한 결과는 JUnit의 assertion을 사용해서 점검하고 있다. 두번째 getTopParent() 오퍼레이션 또한 동일한 형태로 테스트 코드가 작성되었다.

SaveRepository의 구현 클래스인 SaveRepoImpl 클래스의 코드는 다음과 같다.
public class SaveRepoImpl extends HibernateDaoSupport implements SaveRepository {
 @Override
 public boolean saveTopParent(TopParentObject tpo) {
  getHibernateTemplate().save(tpo);
  return true;
 }
 @Override
 public TopParentObject getTopParent(String id) {
  TopParentObject result = (TopParentObject)getHibernateTemplate().get(TopParentObject.class, id);
  if (result != null) {
   Hibernate.initialize(result.getMidObjects());
   
   for (MidParentObject mid : result.getMidObjects()) {
    Hibernate.initialize(mid.getChildern());
   }
  }
  return result;
 }
}
위의 코드는 이전 일대다 매핑에서 소개했었다. 위의 코드를 실행하기 위해서는 Spring 관련 설정파일이 있어야 된다. 다음은 Spring 관련 설정 파일이다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
 <bean id="myDataSource"
  class="org.unitils.database.UnitilsDataSourceFactoryBean" />

 <bean id="mySessionFactory"
  class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
  <property name="dataSource" ref="myDataSource" />
  <property name="annotatedClasses">
   <list>
    <value>kr.nextree.domain.TopParentObject</value>
    <value>kr.nextree.domain.MidParentObject</value>
    <value>kr.nextree.domain.ChildObject</value>
   </list>
  </property>
  <property name="hibernateProperties">
   <value>
    hibernate.dialect=org.hibernate.dialect.HSQLDialect
    hibernate.show_sql=true
    hibernate.hbm2ddl.auto=create-drop
   </value>
  </property>
 </bean>
 <bean id="txManager"
  class="org.springframework.orm.hibernate3.HibernateTransactionManager">
  <property name="sessionFactory" ref="mySessionFactory" />
 </bean>
 <bean id="hibernateTemplate"
  class="org.springframework.orm.hibernate3.HibernateTemplate">
  <property name="sessionFactory">
   <ref bean="mySessionFactory" />
  </property>
 </bean>
 <bean id="saveRepository"
  class="kr.nextree.repo.hibernate.SaveRepoImpl">
  <property name="hibernateTemplate">
   <ref bean="hibernateTemplate" />
  </property>
 </bean>
 <bean id="txProxyTemplate"
  class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
  abstract="true">
  <property name="transactionManager" ref="txManager" />
  <property name="transactionAttributes">
   <props>
    <prop key="save*">PROPAGATION_REQUIRED</prop>
    <prop key="*">PROPAGATION_SUPPORTS,readOnly</prop>
   </props>
  </property>
 </bean>
 <bean id="saveRepositoryTx" parent="txProxyTemplate">
  <property name="target" ref="saveRepository" />
  <property name="proxyInterfaces"
   value="kr.nextree.repo.SaveRepository" />
 </bean>
</beans>
위의 설정 파일에 대한 자세한 내용을 생략하기로 한다. 자세한 내용을 Spring 프레임워크 관련 문서를 참조하기 바란다.

그 다음에는 이 Hibernate 구현 클래스에 대한 테스트 케이스는 Unitils 를 사용해서 다음과 같이 코드를 작성한다.
@Transactional(TransactionMode.ROLLBACK)
@SpringApplicationContext({"spring-hibernate.xml"})
public class SaveRepoImplTest extends UnitilsJUnit4 {
 @SpringBean(value="saveRepository")
 private SaveRepository saveRepo;
 
 @SpringBean(value="saveRepositoryTx")
 private SaveRepository saveRepo2;
 
 @Test public void saveTopParent() throws Exception {
  TopParentObject tpo = new TopParentObject("id", "field");
 
  MidParentObject mpo1 = new MidParentObject("1", "f1", tpo);
  mpo1.addChild(new ChildObject("1", "aaa", mpo1));
  mpo1.addChild(new ChildObject("2", "bbb", mpo1));
  tpo.addMidObject(mpo1);
 
  MidParentObject mpo2 = new MidParentObject("2", "f2", tpo);
  mpo2.addChild(new ChildObject("3", "aaa", mpo2));
  mpo2.addChild(new ChildObject("4", "ccc", mpo2));
  tpo.addMidObject(mpo2);
 
  assertTrue(saveRepo.saveTopParent(tpo));
 }
 
 @DataSet ("SaveRepoImplTest.getTopParent.xml")
 @Test public void getTopParent() throws Exception {
  TopParentObject tpo = saveRepo2.getTopParent(new String("1"));
  assertNotNull(tpo);
 
  List<MidParentObject> mids = tpo.getMidObjects();
  assertNotNull(mids);
  assertEquals(2, mids.size());
 
  MidParentObject mid1 = mids.get(0);
  assertEquals(new String("1"), mid1.getId());
  List<ChildObject> children = mid1.getChildern();
  assertNotNull(children);
  assertEquals(2, children.size());
 }
}
위의 테스트 코드에서 보는 바와 같이 테스트케이스는 org.unitils.UnitilsJUnit4를 상속받고 있다. 또한, Unitils에서 제공하는 트랜잭션 처리를 위해서 @Transactional 을 사용하고 있으며, Spring 설정 파일을 위해서 @SpringApplicationContext 을 사용하고 있다. 즉, 위의 Spring 설정 파일은 클래스패스 제일 상위에 spring-hibernate.xml 이라는 이름으로 저장되어 있어야 한다. Spring의 Bean을 사용하기 위한 DI(Dependency Injection)은 @SpringBean 라는 annotation을 사용한다. 기타 나머지는 JUnit의 일반적인 코드와 동일하다.
마지막으로, 조회에 대한 테스트시 테스트 데이터에 대한 로딩을 위해서 @DataSet이라는 annotation을 사용하는데, 이는 클래스 전체에 대한 테스트 데이터를 위해서 클래스위에 선언할 수도 있으며, 테스트 메소드만을 위해서 테스트 메소드 위에만 설정할 수도 있다. 이 annotation은 DBUnit을 사용하고 있다. 따라서, SaveRepoImplTest.getTopParent.xml 파일은 다음과 같이 DBUnit을 사용할 수 있게끔 테스트 데이터를 지정하면 된다.
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
 <TOP_TAB id="1" field="aaa"/>
 
 <MID_TAB id="1" parent_id="1" field="abcd"/>
 <MID_TAB id="2" parent_id="1" field="efgh"/>
 
 <CHILD_TAB id="1" parent_id="1" parent_parent_id="1" field="text1"/>
 <CHILD_TAB id="2" parent_id="1" parent_parent_id="1" field="text2"/>
 <CHILD_TAB id="3" parent_id="2" parent_parent_id="1" field="text3"/>
</dataset>
단, @DataSet에서 지정된 XML 위치는 해당 테스트케이스 소스파일이 위치한 패키지와 동일하게 저장되어야 한다.

마지막으로, 위의 테스트는 실제 DB가 아닌 HSQLDB의 메모리 DB를 사용함으로써 실제 DB와 연결하지 않고서 DB관련 테스팅이 가능하다. 이를 위해서 다음과 같은 unitils.properties 파일이 필요하다. (이 파일은 클래스패스 제일 상위에 위치하면 된다.)
database.driverClassName=org.hsqldb.jdbcDriver
database.url=jdbc:hsqldb:mem:test
database.userName=sa
database.password=
database.schemaNames=public
database.dialect=hsqldb
DatabaseModule.Transactional.value.default=commit
transactionManager.type=simple

트랜잭션의 경우, Spring을 사용하려면 맨 마지막의 transactionManager.type=spring으로 세티하면 된다.

아래는 Eclipse 에서 파일의 위치이다.

사용자 삽입 이미지

위의 세팅은 maven 2.0을 사용하고 있다.

HibernateTest.zip


반응형