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

(前回の続き)

これまで、テスト対象クラスではMapのListを返すようになっていたが、これをBeanのListを返すように変更する。
この記事はテストがメインなのでテスト対象クラスがどのような形式であろうが(どのようなO/Rマッパを使おうが)関係ないのだが、普通はBeanを返すだろうことと、用意するデータとテスト対象クラスが返す結果を合わせた方がいいかなと思いそこから始めることにする。

まずは Bean クラスとして Sample を用意する

```java:src/main/java/jp/sample/Sample.java 
package jp.sample;

public class Sample {
    int id;
    String str;

    public int getId() {
        return id;
    }

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

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }
}
```

次に、テスト対象クラスも queryForList() を query() に変え、RowMapper が返却した Sample オブジェクトのListを返すように変更する。

```java:src/main/java/jp/sample/JavaDBSample4.java
package jp.sample;

import java.util.List;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
 
@Component
public class JavaDBSample4 {
  @Autowired
  JdbcTemplate jdbcTemplate;
   
  @Transactional
  public List<Sample> select() {
    return jdbcTemplate.query("select * from sample4", 
      new RowMapper<Sample>() {

      @Override
      public Sample mapRow(ResultSet rs, int rowNum) throws SQLException {
        Sample sample = new Sample();
        sample.setId(rs.getInt("ID"));
        sample.setStr(rs.getString("STR"));
        return sample;
      }
    });
  }
}
```


BeanPropertyRowMapperを使う
---------------------------

上記では、RowMapper インタフェースを利用しているが、Beanのプロパティ名とデータベースの項目名が一致するなら、BeanPropertyRowMapper をぜひ使うべきである([convention over configuration](http://ja.wikipedia.org/wiki/%E8%A8%AD%E5%AE%9A%E3%82%88%E3%82%8A%E8%A6%8F%E7%B4%84))。その場合、select()メソッドは以下だけで良くなる

```java:
  public List<Sample> select() {
    return jdbcTemplate.query("select * from sample4", 
        new BeanPropertyRowMapper(Sample.class));
  }
```

テスト用のテーブルは sample4 に変更した

```sql:src/test/resources/create.sql
drop table sample4
create table sample4 (id numeric(3), str varchar(5))
```
```sql:src/test/resources/insert.sql
insert into sample4 values (4, 'd')
```

テストクラスは以下のとおりである。検証する部分が変わるだけで前回とそんなに違わない

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

import java.math.BigDecimal;
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.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
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() {
        executeScript("/insert.sql");

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

        assertThat(samples.size(), is(1));
        assertThat(samples.get(0).getId(), is(4));
        assertThat(samples.get(0).getStr(), is("d"));
    }

    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);
    }
}
```

LomBokを使う
-----------

私は、JavaのgetXXX, setXXX なんてものが好きではない。こんなものは言語側が勝手に用意して欲しいと思っている。以降は、それをJavaで実現するための[LomBok](http://projectlombok.org/)を利用することにする。

何がうれしいかというと Sample.java が以下だけになる。`@Data`アノテーションが肝であり、これを付けるだけ(import もいるけど普通IDEでやってくれる)で煩わしいgetter/setterが要らなくなる。

```java:src/main/java/jp/sample/Sample.java
package jp.sample;

import lombok.Data;

@Data
public class Sample {
    int id;
    String str;
}
```

このためには、LomBok の jar を取得し、コンパイル時にLomBokの魔法を掛ける必要がある。

この記事で利用する Gradle では、
    ```compile "org.projectlombok:lombok:1.12.+"```
を dependencies に追加することになる。
(lombok はコンパイル時に必要なライブラリであり、実行時には不要なのだが Gradle の標準ではその指定ができない。`provided` という命令を追加する記事も見かけるが、ここでは本質的ではないので `compile` にて、実行時にもlombok.jarをクラスパスに通したままにする)

```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.+"
}
```

LomBok を使いたくない場合はBeanにgetter/setter(そして今後必要になるであろう equals(), hashCode())を各自で実装すればいい。それだけの話しである。
(実際、GWT と LomBok を一緒に使うとハマりどころが出たりするなど([google-web-toolkit Issue 5475](https://code.google.com/p/google-web-toolkit/issues/detail?id=5475))使わないという選択肢はありだと思う)

Beanでテストデータを投入する
-------------------------

前回まではテストデータの準備に生のINSERT文を使っていた。これはこれで使えるのだが、ここではBeanからデータを投入する方法を確認する。

まずは、素の JdbcTemplate を使った方法を確認する

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

import java.math.BigDecimal;
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.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
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() {
        Sample sample = new Sample();
        sample.setId(4);
        sample.setStr("d");

        insert(sample);

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

        assertThat(samples.size(), is(1));
        assertThat(samples.get(0).getId(), is(4));
        assertThat(samples.get(0).getStr(), is("d"));
    }

    public void insert(Sample sample) {
        jdbcTemplate.update("insert into sample4(id, str) values (?, ?)",
            new Object[] { sample.getId(), sample.getStr()});
    }

    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);
    }
}
```

変わっているのは

```java:
        Sample sample = new Sample();
        sample.setId(4);
        sample.setStr("d");

        insert(sample);
```

っとbeanで値を設定している箇所と

```java:
    public void insert(Sample sample) {
        jdbcTemplate.update("insert into sample4(id, str) values (?, ?)",
            new Object[] { sample.getId(), sample.getStr()});
    }
```

上記のメソッドでINSERTしている箇所である。

これは機能するが、insert 文と bean のプロパティの対応がコードに暗に定義されているところが気に食わない。これは NamedParameterJdbcTemplate と BeanPropertySqlParameterSource を使えば解決することができる。





NamedParameterJdbcTemplateを使う
-----------------------------
具体的には、insert() メソッドを以下のように書き換えるだけである。

```java
    public void insert(Sample sample) {
        new NamedParameterJdbcTemplate(jdbcTemplate).update(
            "insert into sample4(id, str) values (:id, :str)",
            new BeanPropertySqlParameterSource(sample));
    }
```

ScriptUtilsを使う
----------------
あと問題なのはせっかく外出ししたinsert文およびテストデータが再度コードに入ってしまっている(もちろんその方が良い場合もある)。まずはinsert文を再度外に出してみよう。

```sql:src/test/resources/insert.sql
insert into sample4(id, str) values (:id, :str)
```

テストクラス側はこのファイルを読み込むようにしてみよう。調べてみたが ScriptUtils でファイルからSQL文を読み取るくらいしか方法が見つからなかった。
これはとても低レベルなインタフェースなので、ClassPathResource から readScript するまでに結構手間がかかる。
また、deprecated な JdbcTestUtils.readScript() と比べて コメント文字や行区切り文字を指定しなければならないなどちょっと使い勝手が悪い気がする。
あまり納得はできないが、一応以下のようにしてできた。

```java:
    public void insert(String file, Sample sample) throws IOException {
        Resource resource = new ClassPathResource(file, getClass());
        try (
            LineNumberReader reader = new LineNumberReader(new InputStreamReader(resource.getInputStream()));
        ) {

            String script = ScriptUtils.readScript(reader, "--", ";");
            new NamedParameterJdbcTemplate(jdbcTemplate).update(
                script,
                new BeanPropertySqlParameterSource(sample));
        }
    }
```


あとは、テストデータを外出し出来ると良さそうだ。

次回、そのあたりを解決したい