テストケースからのH2起動

H2の組み込みモードで単体テスト

単体テストとか、結合テストとかでテストスイート起動時にデータベースを起動して、終了時にデータベースをシャットダウンする例です。

@RunWith(Suite.class)
@SuiteClasses( {
    ProductDaoImplTest.class,
    ProductServiceImplTest.class
})
public class AllTests {

    private static org.h2.tools.Server server;
    
    @BeforeClass
    public static void beforeClass() throws Exception {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                shutdownServer();
            }
        }));

        server = org.h2.tools.Server.createTcpServer(
                new String[] { "-baseDir", "db\\h2", "-tcpPort", "9092" }).start();
    }

    @AfterClass
    public static void afterClass() {
        shutdownServer();
    }
    
    private static void shutdownServer() {
        if (server != null) {
            server.shutdown();
            server = null;
        }
    }
}

H2をテストスイートから組み込みモードで立ち上げています。AfterClassでH2のシャットダウンを行っていますが、JUnitがエラーで終了した時のために、addShutdownHook()にて VM 終了時にもシャットダウン処理をしています。
昔は単体テストがDBに依存するのをきらって、モックをしこしこと作成していたものですが、H2であればライブラリの様に扱うことができ、ORMを使えばデータベースの方言も吸収してくれるのでとっても楽です。

H2をインメモリで使用して単体テスト

H2 は JDBC の接続 URL の指定で簡単にインメモリデータベースとして使えます。Springだと、アプリケーションコンテキストで以下の様な指定とします。

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="org.h2.Driver"/>
    <property name="url" value="jdbc:h2:mem:test"/>
    <property name="username" value="sa"/>
    <property name="password" value=""/>
  </bean>

jdbc:h2:mem: でコネクションが作成されると勝手にインメモリでデータベースが動きます。Hibernate
で、「hibernate.hbm2ddl.auto = create」とかしておくと、テストケースの実行がとても手軽になります。

H2による単体テストのサンプル

前述の動きの確認用のサンプルを書いておきます。Spring2.5 Hibernate3 JUnit4 H2 の組み合わせです。

エンティティとなるProductクラス
package etc9.domain.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Product {
    private Long id;
    private String name;
    private String category;
    
    public Product(){}
    public Product(String name, String category) {
        this.name = name;
        this.category = category; 
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
}
Product を操作するDao
package etc9.dao;

import java.util.List;
import etc9.domain.entity.Product;

public interface ProductDao {
	List<Product> findByCategory(final String category);
	Product get(Long id);
	Product save(Product product);
}


package etc9.dao;

import java.sql.SQLException;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;
import etc9.domain.entity.Product;

public class ProductDaoImpl implements ProductDao {
    private HibernateTemplate hibernateTemplate;

    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.hibernateTemplate = new HibernateTemplate(sessionFactory);
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public List<Product> findByCategory(final String category) {
        return (List<Product>) this.hibernateTemplate.execute(new HibernateCallback(){
            @Override public Object doInHibernate(Session session)
                    throws HibernateException, SQLException {
                Criteria criteria = session.createCriteria(Product.class);
                criteria.add(Restrictions.eq("category", category));
                return criteria.list();
            }});
    }

    @Override public Product get(final Long id) {
        return (Product) this.hibernateTemplate.get(Product.class, id);
    }
    
    @Override public Product save(final Product product) {
        this.hibernateTemplate.save(product);
        return product;
    }
}
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.5.xsd">

  <context:annotation-config/> 

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="org.h2.Driver"/>
    <property name="url" value="jdbc:h2:mem:test"/>
    <property name="username" value="sa"/>
    <property name="password" value=""/>
  </bean>
 
  <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="hibernateProperties">
      <value>
        hibernate.dialect = org.hibernate.dialect.H2Dialect
        hibernate.show_sql = true
        hibernate.format_sql = false
        hibernate.hbm2ddl.auto = create
      </value>
    </property>
    <property name="annotatedClasses">
     <list>
       <value>etc9.domain.entity.Product</value>
     </list>
    </property>
  </bean>

  <bean id="productDao" class="etc9.dao.ProductDaoImpl"></bean>

</beans>

組み込みモードで使う場合は以下の要領

    <property name="url" value="jdbc:h2:tcp://localhost:9092/test"/>

動作確認用のテストケース

package etc9.dao;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.junit.Before;
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 etc9.domain.entity.Product;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class ProductDaoImplTest {

    @Autowired
    private ProductDao dao;
    
    @Before
    public final void before() {
        Product product1 = new Product("name1", "category");
        dao.save(product1);
        Product product2 = new Product("name2", "category");
        dao.save(product2);
    }
    
    @Test
    public final void testFindByCategory() {
        for(Product p : dao.findByCategory("category")) {
            System.out.println(ReflectionToStringBuilder.toString(p));
        }
    }
}


テストスイートは先の通り