(前回の続き)
本記事は、考えてみたら「Java + Spring を使ったプロジェクトにおけるテストデータ管理」が正しかったかもしれない。というのもデータベースアクセスに関してSpringを利用する前提としているが、テストデータに関してはSpringにこだわる必要はないからである。
そういうわけで、CSVでテストデータを準備するためにopencsvを使ってみる。
opencsv を使う
なぜ、opencsvを使うかというと
- CSV から Bean に変換するクラスが用意されている
- 作りがシンプルで理解しやすい
- ソースが公開されているため、自分の都合の良いように変更ができる(ここでもシンプルであることに利点がある)
まずは、build.gradle である。これは testCompile "net.sf.opencsv:opencsv:2.+"
を dependencies に付け足せばいいだけである。
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を作成するためのユーティリティクラスを用意する。
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);
などとなる。総称型に関して詳しくはここを見ると良い。
このTestDataUtilsを使うと、テストクラスは以下のようになる。
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
して動くことを確認したら少し整理しよう。
- テストデータをcreate.sql や insert.sql と同じところで管理する
- executeScript(), insert() などのメソッドは TestDataUtilsに押し込む
まずは、1 を試す。
テストデータであるCSVファイルをクラスパスから取得しよう。
これは、executeScript()メソッドと同じ方法で、springのClassPathResourceを使えば良さそうだ。
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ファイルを以下のように用意し、呼び出し側でそのパスを指定すれば良い。(ソース全体は最後に示そう)
ID,STR
5,e
次に、2.のinsert() および executeScript()メソッドをTestDataUtilsクラスに移動する。
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に押し込んだので、テスト本体は以下のようにシンプルにできた。
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) は重複がある。テーブル項目定義を変更すれば、これらは連動して変更しなければならない。
- テストメソッドに対応するテストパターンの対応付けにルールを設けたい。
次回以降、そんな効率化について検討していきたい。