Spring Test DBUnit でテスト毎にテーブルを用意する方法


## 各バージョン
訳あって`Spring`のバージョンが`3.2.8.RELEASE`ですが、4系の最新版でも問題ないです。

その場合、Spring Test DBUnitのバージョンは、`1.3.0`が良いです。

 * Java
 1.8.0 update 92
 
 * Spring Framework
 3.2.8.RELEASE
  
 * [Spring Test DBUnit](https://springtestdbunit.github.io/spring-test-dbunit/)
 1.0.1

 * DbUnit
 2.5.2

 * H2 Database
 1.4.191

 * JUnit
 4.12
 
## 背景
`Spring` + `DbUnit` + `Spring Test DBUnit` を使ってDB関連クラスの単体テストを行う場合に、
各開発者の環境やCI環境においてそれぞれの環境でDBを用意するのは大変なので、
インメモリな`H2 Database`を採用することは少なくないと思っています。

``` xml:application.xml
    <bean id="dataSource"
          class="org.h2.jdbcx.JdbcDataSource">
        <property name="URL" value="jdbc:h2:mem:sample;MODE=MySQL"/>
        <property name="user" value="user"/>
        <property name="password" value="password"/>
    </bean>

    <jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
        <jdbc:script location="classpath:schema.sql" />
    </jdbc:initialize-database>
```

``` java:SampleTest.java
package jp.ijufumi.test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import jp.ijufumi.test.support.DatabaseInitTestExecutionListener;

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:META-INF/spring/applicationContext*.xml")
@Transactional
@TransactionConfiguration
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class})
public class SampleTest {
   @Test
   public void test001() {
   }

   @Test
   public void test002() {
   }
}
```

その際に、上記のような設定になること多くて、テストを実行するたびに全テーブルを作成してしまい、
あまり効率的ではないです。

## 実現方法
そこで、次のようなクラス、アノテーションを作成することで、テスト毎に必要なテーブルのみ用意することができます。

``` xml:application.xml
    <bean id="dataSource"
          class="org.h2.jdbcx.JdbcDataSource">
        <property name="URL" value="jdbc:h2:mem:sample;MODE=MySQL"/>
        <property name="user" value="user"/>
        <property name="password" value="password"/>
    </bean>
```

``` java:DatabaseInitTestExecutionListener.java
package jp.ijufumi.test.support;

import javax.sql.DataSource;

import jp.ijufumi.test.annotation.DbSchemes;

import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;

public class DatabaseInitTestExecutionListener extends AbstractTestExecutionListener {
    private Logger logger = LoggerFactory.getLogger(getClass());

    private static final String BASE_PATH = "data/db/schema/";

    private static final String FILE_SUFFIX = ".sql";

    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        logger.debug("DB Schema setup for {}", testContext.getTestMethod().getName());
        DbSchemes dbSchemes = testContext.getTestMethod().getAnnotation(DbSchemes.class);
        if (dbSchemes == null) {
            logger.debug("@DbSchemes is not exists.");
            return;
        }

        String dataSourceName = "dataSource";
        DbUnitConfiguration configuration = testContext.getTestClass().getAnnotation(DbUnitConfiguration.class);
        if (configuration != null && StringUtils.isNotEmpty(configuration.databaseConnection()))
        {
            dataSourceName = configuration.databaseConnection();
        }

        DataSource dataSource = (DataSource) testContext.getApplicationContext().getBean(dataSourceName);
        ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();

        for (String s : dbSchemes.value())
        {
            logger.debug("Create table : {}", s);
            databasePopulator.addScript(new ClassPathResource(BASE_PATH + s + FILE_SUFFIX));
        }
        DatabasePopulatorUtils.execute(databasePopulator, dataSource);
    }
}
```

``` java:DbSchemas.java
package jp.ijufumi.test.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface DbSchemes {
    String[] value();
}

```

``` java:SampleTest.java
package jp.ijufumi.test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import jp.ijufumi.test.support.DatabaseInitTestExecutionListener;

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:META-INF/spring/applicationContext*.xml")
@Transactional
@TransactionConfiguration
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DatabaseInitTestExecutionListener.class,
        DbUnitTestExecutionListener.class})
public class SampleTest {
   @Test
   @DbSchemas("table1")
   public void test001() {
   }

   @Test
   @DbSchemas({"table1", "table2"})
   public void test002() {
   }
}
```

で、以下のようなDDLを用意します。
格納場所は、`{project.dir}/src/test/resources/data/db/schema/SampleTest`で、拡張子は`.sql`です。
この辺は、`DatabaseInitTestExecutionListener`の定数で定義しているので、使う人によって変更していけばいいのかと思います。


``` sql:table1.sql
create table if not exists table1 (
   id varchar(10) not null,
   name varchar(100),
   create_date datetime,
   primary key (id)
);
```

``` sql:table2.sql
create table if not exists table1 (
   id varchar(10) not null,
   name varchar(100),
   category varchar(3),
   create_date datetime,
   primary key (id)
);
```

## 所感
今回は、必要なテーブルを用意するということが目的でしたが、
`Spring Test`の`ExecutionListener`の仕組みはすごく便利で
今後色々使っていきたいなと思いました。

## お願い
もし、私のやり方よりもっと効率のいいやり方あったらコメント下さい。
コメントいただけたら随時反映していきたいと思っています。