Java + Spring によるテストデータ管理(5)

(前回の続き)

本記事は、考えてみたら「Java + Spring を使ったプロジェクトにおけるテストデータ管理」が正しかったかもしれない。というのもデータベースアクセスに関してSpringを利用する前提としているが、テストデータに関してはSpringにこだわる必要はないからである。

そういうわけで、CSVでテストデータを準備するために[opencsv](http://opencsv.sourceforge.net/)を使ってみる。

opencsv を使う
-------------

なぜ、opencsvを使うかというと

* CSV から Bean に変換するクラスが用意されている
* 作りがシンプルで理解しやすい
* ソースが公開されているため、自分の都合の良いように変更ができる(ここでもシンプルであることに利点がある)

まずは、build.gradle である。これは `testCompile "net.sf.opencsv:opencsv:2.+"`
を dependencies に付け足せばいいだけである。

```groovy:build.gradle
apply plugin: 'java'

repositories {
    mavenCentral()
}

ext {
    springVersion = '4.0.5.RELEASE'
}

dependencies {
    compile "org.springframework:spring-beans:$springVersion"
    compile "org.springframework:spring-context:$springVersion"
    compile "org.springframework:spring-context-support:$springVersion"
    compile "org.springframework:spring-tx:$springVersion"
    compile "org.springframework:spring-jdbc:$springVersion"


    compile 'org.apache.derby:derby:10.+'

    testCompile 'junit:junit:4.+'
    testCompile "org.springframework:spring-test:$springVersion"

    compile "org.projectlombok:lombok:1.12.+"

    testCompile "net.sf.opencsv:opencsv:2.+"
}
```

続いて、CSVからBeanを作成するためのユーティリティクラスを用意する。

```java:src/test/java/jp/sample/TestDataUtils.java
package jp.sample;

import java.io.Reader;
import java.util.List;

import au.com.bytecode.opencsv.bean.HeaderColumnNameMappingStrategy;
import au.com.bytecode.opencsv.bean.CsvToBean;

public class TestDataUtils {
    public static <T> List<T> getBeans(Class<T> clazz, Reader reader) {
        HeaderColumnNameMappingStrategy<T> strategy = new HeaderColumnNameMappingStrategy<T>();
        strategy.setType(clazz);

        CsvToBean<T> csv = new CsvToBean<T>();
        return csv.parse(strategy, reader);
    }
}
```

いくつかポイントがある。

* opencsv の使い方として、HeaderColumnNameMappingStrategyにより、CSVファイルの1行目のヘッダから項目名を取得しBeanへのマップに利用する。CsvToBean#parse でCSVを読み込んで Bean のListを返す。
* 汎用のメソッドとするためにBeanの型は総称型Tで定義する。あまり見ないが、staticメソッドの総称型で定義してみた。呼び出し方は `TestDataUtils.getBeans(Sample.class, reader);` などとなる。総称型に関して詳しくは[ここ](http://www.ne.jp/asahi/hishidama/home/tech/java/generics.html)を見ると良い。

このTestDataUtilsを使うと、テストクラスは以下のようになる。

```java:src/test/java/jp/sample/JavaDBSample4Test.java
package jp.sample;

import java.io.InputStreamReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.StringReader;
import java.sql.Connection;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.ContextConfiguration;

import org.junit.runner.RunWith;
import org.junit.*;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
public class JavaDBSample4Test {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    JavaDBSample4 obj;

    @Before
    public void setUp() {
        executeScript("/create.sql");
    }

    @Test
    public void test() throws IOException {
        String s = ""+
            "id,str" + "\n" +
            "5,e" + "\n";

        List<Sample> samplesIn = TestDataUtils.getBeans(Sample.class, new StringReader(s));

        insert("/insert.sql", samplesIn);

        List<Sample> samplesOut = obj.select();

        assertThat(samplesOut.size(), is(1));
        assertThat(samplesOut.get(0).getId(), is(5));
        assertThat(samplesOut.get(0).getStr(), is("e"));
        assertEquals(samplesIn, samplesOut);
    }

    public void insert(String file, List<Sample> samples) throws IOException {
        Resource resource = new ClassPathResource(file, getClass());
        String script;
        try (
            LineNumberReader reader = new LineNumberReader(new InputStreamReader(resource.getInputStream()));
        ) {

            script = ScriptUtils.readScript(reader, "--", ";");
        }

        NamedParameterJdbcTemplate npjt = new NamedParameterJdbcTemplate(jdbcTemplate);

        for (Sample sample: samples) {
            npjt.update(script, new BeanPropertySqlParameterSource(sample));
        }
    }

    public void executeScript(String file) {
        Resource resource = new ClassPathResource(file, getClass());

        ResourceDatabasePopulator rdp = new ResourceDatabasePopulator();
        rdp.addScript(resource);
        rdp.setSqlScriptEncoding("UTF-8");
        rdp.setIgnoreFailedDrops(true);
        rdp.setContinueOnError(false);

        Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
        rdp.populate(conn);
    }
}
```

前回からの差分は以下。

* csvファイルはまだ準備段階とし、Stringでソースに埋め込んだ。
* 入力データ samplesIn を先ほど用意した `TestDataUtils.getBeans()`で取得するようにした
* insert() メソッドは、Sample から List\<Sample\> を受け取るように変更した。
* 入力データ samplesIn と samplesOut が一致することを確認するために、assertEquals() を使ってみた。これは LomBok によりBeanにequals()メソッドが再定義されているためにうまく行く。

`gradle check` して動くことを確認したら少し整理しよう。

1. テストデータをcreate.sql や insert.sql と同じところで管理する
2. executeScript(), insert() などのメソッドは TestDataUtilsに押し込む

まずは、1 を試す。

テストデータであるCSVファイルをクラスパスから取得しよう。
これは、executeScript()メソッドと同じ方法で、springのClassPathResourceを使えば良さそうだ。

```java
    public static <T> List<T> getBeans(Class<T> clazz, String csvPath) throws IOException {
        Resource resource = new ClassPathResource(csvPath, clazz);

        HeaderColumnNameMappingStrategy<T> strategy = new HeaderColumnNameMappingStrategy<T>();
        strategy.setType(clazz);

        CsvToBean<T> csv = new CsvToBean<T>();
        try (Reader reader = new InputStreamReader(resource.getInputStream())) {
            return csv.parse(strategy, reader);
        }
    }
```

ClassPathResource() の第2引数に渡すクラスクラスはBeanのクラスを流用した。イマイチな気はするがとりあえず動くので良しとする。(後ですぐに修正する)

CSVファイルを以下のように用意し、呼び出し側でそのパスを指定すれば良い。(ソース全体は最後に示そう)

```src/test/resources/sample.csv 
ID,STR
5,e
```

次に、2.のinsert() および executeScript()メソッドをTestDataUtilsクラスに移動する。

```java:src/test/java/jp/sample/TestDataUtils.java 
package jp.sample;

import java.io.InputStreamReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.sql.Connection;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.jdbc.datasource.init.ScriptUtils;

import au.com.bytecode.opencsv.bean.HeaderColumnNameMappingStrategy;
import au.com.bytecode.opencsv.bean.CsvToBean;

public class TestDataUtils {
    DataSource dataSource;

    public TestDataUtils(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public <T> List<T> getBeans(Class<T> clazz, String csvPath) throws IOException {
        Resource resource = new ClassPathResource(csvPath, getClass());

        HeaderColumnNameMappingStrategy<T> strategy = new HeaderColumnNameMappingStrategy<T>();
        strategy.setType(clazz);

        CsvToBean<T> csv = new CsvToBean<T>();
        try (Reader reader = new InputStreamReader(resource.getInputStream())) {
            return csv.parse(strategy, reader);
        }
    }

    public void insert(String file, List<Sample> samples) throws IOException {
        Resource resource = new ClassPathResource(file, getClass());
        String script;
        try (
            LineNumberReader reader = new LineNumberReader(new InputStreamReader(resource.getInputStream()));
        ) {

            script = ScriptUtils.readScript(reader, "--", ";");
        }

        NamedParameterJdbcTemplate npjt = new NamedParameterJdbcTemplate(dataSource);

        for (Sample sample: samples) {
            npjt.update(script, new BeanPropertySqlParameterSource(sample));
        }
    }

    public void executeScript(String file) {
        Resource resource = new ClassPathResource(file, getClass());

        ResourceDatabasePopulator rdp = new ResourceDatabasePopulator();
        rdp.addScript(resource);
        rdp.setSqlScriptEncoding("UTF-8");
        rdp.setIgnoreFailedDrops(true);
        rdp.setContinueOnError(false);

        Connection conn = DataSourceUtils.getConnection(dataSource);
        rdp.populate(conn);
    }
}
```

移動に伴い、以下の変更も行った。

* insert(), executeScript() をインスタンスメソッドにして、dataSourceやクラスローダを共通化した。そこで、getBeans() も同じようにインスタンスメソッドにする。
* 引数をJdbcTemplate ではなくDataSourceに変更する。その方が汎用性が高まるかなと思ったからである。

雑多なものをほとんどTestDataUtilsに押し込んだので、テスト本体は以下のようにシンプルにできた。

```java:src/test/java/jp/sample/JavaDBSample4Test.java
package jp.sample;

import java.io.IOException;
import java.io.Reader;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.ContextConfiguration;

import org.junit.runner.RunWith;
import org.junit.*;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
public class JavaDBSample4Test {

    @Autowired
    DataSource dataSource;

    @Autowired
    JavaDBSample4 obj;

    TestDataUtils td;

    @Before
    public void setUp() {
        td = new TestDataUtils(dataSource);
        td.executeScript("/create.sql");
    }

    @Test
    public void test() throws IOException {
        List<Sample> samplesIn = td.getBeans(Sample.class, "/sample.csv");

        td.insert("/insert.sql", samplesIn);

        List<Sample> samplesOut = obj.select();

        assertThat(samplesOut.size(), is(1));
        assertThat(samplesOut.get(0).getId(), is(5));
        assertThat(samplesOut.get(0).getStr(), is("e"));
        assertEquals(samplesIn, samplesOut);
    }
}
```

だいぶ形ができてきた。なんとなくTestDataUtilsなんてクラスも作成できて、いよいよ効率化という点にクローズアップできそうな気がしてきた。

今後、解決したい点は以下である。

* テーブル定義(create.sql), データ投入(insert.sql), テストデータ(sample.csv) は重複がある。テーブル項目定義を変更すれば、これらは連動して変更しなければならない。
* テストメソッドに対応するテストパターンの対応付けにルールを設けたい。

次回以降、そんな効率化について検討していきたい。