LoginSignup
13
10

More than 5 years have passed since last update.

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

Posted at

(前回の続き)

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

そういうわけで、CSVでテストデータを準備するためにopencsvを使ってみる。

opencsv を使う

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

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

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

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を作成するためのユーティリティクラスを用意する。

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); などとなる。総称型に関して詳しくはここを見ると良い。

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

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を使えば良さそうだ。

    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クラスに移動する。

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に押し込んだので、テスト本体は以下のようにシンプルにできた。

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) は重複がある。テーブル項目定義を変更すれば、これらは連動して変更しなければならない。
  • テストメソッドに対応するテストパターンの対応付けにルールを設けたい。

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

13
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10