Spring + Mybatis + DbUnit を使ってみた

Spring + Mybatis環境でDB周りの試験をする機会があり、DbUnitを使う方法を調べました。
(自分が忘れないためにも載せておきます)
トランザクションをSpring側で管理するため、DbUnitとの連携は少し設定が必要です。

## 動作環境
|MW      |  Version |
|:---    |---------:|
|Java    |         7|
|postgresql | 9.5|
|spring-core|   4.3.7.RELEASE|
|mybatis | 3.4.2|
|mybatis-spring | 1.3.1|
|spring-jdbc | 4.3.7.RELEASE|
|spring-test | 4.3.7.RELEASE|
|dbunit | 2.5.3|
|spring-context | 4.3.7.RELEASE|
|postgresql(jdbc Driver) | 42.0.0.jre7|

## Beanファイルの設定
springとDbUnitを連携するにはspringのbeans定義ファイルに以下のような定義を追加します。
DbUnitとspringはお互いにトランザクションを管理しているようで、下記のように
TransactionAwareDataSourceProxyクラスでdataSourceをラップしなければDbUnitで設定したデータがロールバックしません。

```XML:applicationContext.xml

    <!-- データソースの設定(参考) -->
   <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="driverClassName" value="org.postgresql.Driver"/>
      <property name="url" value="jdbc:postgresql://localhost:5432/<DB名>" />
      <property name="username" value="<環境依存>" />
      <property name="password" value="<環境依存>" />
   </bean>

   <!-- springと一緒にdbunitを使うときは以下のようにdataSourceをラップする -->
   <bean id="dataSourceTest"
    class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
    <constructor-arg ref="dataSource" />
   </bean>
```

## テストソース
Junitのテストソースの設定についてです。

@RunWithなどのアノテーションはspringを使うときと同様の設定をします。
クラスは、DbUnitを使用するためDataSourceBasedDBTestCaseを継承します。

```Java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@Transactional
public class DocumentMapperTest extends DataSourceBasedDBTestCase {
```

テストの対象のMapperクラスとDataSourceBasedDBTestCaseを継承した際に、実装が必要となったgetDataSource()の返却値として使用するためのdataSourceTestを@AutowiredでDIされるように設定しておきます。

```Java
    @Autowired
    DocumentMapper mapper;

    @Autowired
    DataSource dataSourceTest;

    @Rule
    public TestName name = new TestName();
```

@Beforeの中でsuper.setUp()を呼び出せばテスト前にセットアップしてくれます。

```Java
@Before
    public void setUp() throws Exception {
        // super.setUp()をコールすればテスト実行前にDBをsetup可能.
        // setUp()でコールしなければテストメソッド内でsetupが必要になる
        // super.setUp();
    }
```

前文でも出てきましたが、DataSourceBasedDBTestCaseクラスの継承で実装が必要となったgetDataSource()です。ここでは、@Autowiredで設定しているdataSourceTestを返すようにします。

```Java
    @Override
    protected DataSource getDataSource() {
        return this.dataSourceTest;
    }
```

DataSourceBasedDBTestCaseクラスの継承で実装が必要となったgetDataSet()です。
DBに設定するデータをここでは返却します。super.setUp()内でgetDataSet()が呼ばれます。
ここではクラスパス上からテストメソッド名と一致するエクセルファイル(xls形式)をDBデータとして渡しています。
エクセルファイルにnullと書いているとnullではなく文字列として扱われるためDbUnitが提供するReplacementDataSetクラスを使用してnullに置換しています(エクセルファイルにはnullとする箇所を[null]で定義)。最新のDbUnitでは、xls形式だけではなく、xlsx形式も使用可能です。

```Java
    @Override
    protected IDataSet getDataSet() throws Exception {

        String pathName = name.getMethodName();

        String xlsname = pathName + ".xls";
        InputStream is = getClass().getClassLoader().getResourceAsStream(xlsname);
        XlsDataSet dataSet = new XlsDataSet(is);

        ReplacementDataSet replacement = new ReplacementDataSet(dataSet);
        // [null]をnullに置換
        replacement.addReplacementObject("[null]", null);
        // [systemtime]を現在日時に置換
        replacement.addReplacementObject("[systemtime]", toDateTimeStr(nowTime));

        return replacement;
    }
```

上記の設定をすることで Spring + Mybatis + DbUnitで試験ができるようになります。
実は、[Spring Test DBUnit](https://springtestdbunit.github.io/spring-test-dbunit/)というものがすでにあるのでこちらを使ったほうが無駄な労力をかけなくていいかもしれません。

## 参考(ソース)
```XML:pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>sample</groupId>
  <artifactId>SpringMybatisDbUnit</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <dependencies>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>4.3.7.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.2</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>4.3.7.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>4.3.7.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.dbunit/dbunit -->
    <dependency>
      <groupId>org.dbunit</groupId>
      <artifactId>dbunit</artifactId>
      <version>2.5.3</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.3.7.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <version>42.0.0.jre7</version>
    </dependency>
  </dependencies>
</project>
```

```Java:Document.java
package mng.doc.dao;

import java.lang.reflect.Field;
import java.util.Date;

public class Document {

    private Integer id;
    private String name;
    private String registrant;
    private Date registrationtime;
    private String editor;
    private Date edittime;

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getRegistrant() {
        return registrant;
    }

    public String getEditor() {
        return editor;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setRegistrant(String registrant) {
        this.registrant = registrant;
    }

    public void setEditor(String editor) {
        this.editor = editor;
    }

    public Date getRegistrationtime() {
        return registrationtime;
    }

    public Date getEdittime() {
        return edittime;
    }

    public void setRegistrationtime(Date registrationtime) {
        this.registrationtime = registrationtime;
    }

    public void setEdittime(Date edittime) {
        this.edittime = edittime;
    }

    public String toString() {

        StringBuilder sb = new StringBuilder();
        sb.append("Document [");
        Field[] fields = this.getClass().getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {

            Field field = fields[i];
            String name = field.getName();
            field.setAccessible(true);
            Object value = null;
            try {
                value = field.get(this);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            sb.append(name + " = " + value);
            if (i != (fields.length - 1)) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

```
```Java:DucumentMapper.java
package mng.doc.mapper;

import java.util.Date;
import java.util.List;

import org.apache.ibatis.annotations.Param;

import mng.doc.dao.Document;

public interface DocumentMapper {

    public List<Document> selectAll();
    public Integer insertDocument(Document doc);
    public Integer updateDocument(@Param("editor") String editor, @Param("edittime") Date edittime);
    public Integer deleteDocument(Integer id);
}

```
```Java:DocumentMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mng.doc.mapper.DocumentMapper">

  <resultMap id="baseMap" type="mng.doc.dao.Document">
    <id property="id" jdbcType="INTEGER" column="id" />
    <result property="name" jdbcType="VARCHAR" column="name" />
    <result property="registrant" jdbcType="VARCHAR" column="registrant" />
    <result property="registrationtime" jdbcType="TIMESTAMP" column="registration_time" />
    <result property="editor" jdbcType="VARCHAR" column="editor" />
    <result property="edittime" jdbcType="TIMESTAMP" column="edit_time" />
  </resultMap>

  <select id="selectAll" resultMap="baseMap">
    SELECT
    *
    FROM
    DOCUMENT
  </select>
  <insert id="insertDocument" parameterType="mng.doc.dao.Document">
    INSERT INTO
    DOCUMENT
    (name, registrant, registration_time)
    VALUES
    (
    #{name},
    #{registrant},
    #{registrationtime}

    )
  </insert>
  <update id="updateDocument">
    UPDATE
    DOCUMENT
    SET
    editor = #{editor},
    edit_time = ${edittime}
  </update>

  <delete id="deleteDocument" parameterType="java.lang.Integer">
    DELETE
    FROM
    DOCUMENT
    WHERE
    id = #{id}
  </delete>
</mapper>
```
```Java:DocumentMapperTest.java
package mng.doc.mapper;

import static org.junit.Assert.*;

import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import javax.sql.DataSource;
import mng.doc.dao.Document;
import org.dbunit.DataSourceBasedDBTestCase;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.excel.XlsDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
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("classpath:applicationContext.xml")
@Transactional
public class DocumentMapperTest extends DataSourceBasedDBTestCase {

    Long nowTime = System.currentTimeMillis();

    @Autowired
    DocumentMapper mapper;

    @Autowired
    DataSource dataSourceTest;

    @Rule
    public TestName name = new TestName();


    @Test
    public void testSelectAll() throws Exception {
        prepare();
        List<Document> documents = this.mapper.selectAll();
        for (Document c : documents) {
            System.out.println(c.toString());
        }
    }

    @Before
    public void setUp() throws Exception {
        // super.setUp()をコールすればテスト実行前にDBをsetup可能.
        // setUp()でコールしなければテストメソッド内でsetupが必要になる
        // super.setUp();
    }

    public void prepare() throws Exception {
        DatabaseConnection connection = new DatabaseConnection(this.dataSourceTest.getConnection());
        DatabaseOperation.CLEAN_INSERT.execute(connection, getDataSet());
    }

    public void resetSequenceName(String sequenceName) throws Exception {
        DatabaseConnection connection = new DatabaseConnection(this.dataSourceTest.getConnection());
        connection
        .getConnection()
        .createStatement()
        .executeQuery("select setval('" + sequenceName + "', 1, false)");
    }

    @Override
    protected DataSource getDataSource() {
        return this.dataSourceTest;
    }

    @Override
    protected IDataSet getDataSet() throws Exception {

        String pathName = name.getMethodName();

        String xlsname = pathName + ".xls";
        InputStream is = getClass().getClassLoader().getResourceAsStream(xlsname);
        XlsDataSet dataSet = new XlsDataSet(is);

        ReplacementDataSet replacement = new ReplacementDataSet(dataSet);
        // [null]をnullに置換
        replacement.addReplacementObject("[null]", null);
        // [systemtime]を現在日時に置換
        replacement.addReplacementObject("[systemtime]", toDateTimeStr(nowTime));

        return replacement;
    }

    private String toDateTimeStr(Long time) {
        return toDateTimeStr(new Date(time));
    }

    private String toDateTimeStr(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
        String result = sdf.format(date);
        System.out.println(result);
        return result;
    }
}
```
```XML:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
        http://www.springframework.org/schema/jdbc
        http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd">

    <!-- データソースの設定 -->
   <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="driverClassName" value="org.postgresql.Driver"/>
      <property name="url" value="jdbc:postgresql://localhost:5432/<DB名>" />
      <property name="username" value="<環境依存>" />
      <property name="password" value="<環境依存>" />
   </bean>

  <!-- TransactionManager設定 -->
  <bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
  </bean>

  <!-- SqlSessionFactoryの設定 -->
  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
  </bean>

  <!-- Mapperインターフェースの登録 -->
  <bean id="sampleMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
    <property name="mapperInterface" value="mng.doc.mapper.DocumentMapper" />
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />
  </bean>

  <bean id="dataSourceTest"
    class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
    <constructor-arg ref="dataSource" />
  </bean>

</beans>
```
```SQL:document.sql
-- Table: public.document

-- DROP TABLE public.document;

CREATE TABLE public.document
(
  id integer NOT NULL DEFAULT nextval('document_id_seq'::regclass),
  name text NOT NULL,
  registrant character varying(50),
  registration_time timestamp without time zone,
  editor character varying(50),
  edit_time timestamp without time zone,
  CONSTRAINT document_pkey PRIMARY KEY (id, name)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE public.document
  OWNER TO postgres;

```