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

Java + Spring でテストデータを効率よく管理する方式を模索する

この記事はゆっくり完成させるつもりでいる。

最終目標
--------
* テスト対象はDBアクセス用のORマッパ(主にSQL)とする
* テスト実行の流れは以下の通りとする
  * JUnitでテスト用データを投入する
  * ORマッパがクエリを実行し結果をbeanで返す
  * JUnitのAssertで結果を検証する
* テストデータはテストメソッドと対でフォルダ内で管理する
* テストデータはExcelかCSVで作成する
* テーブル作成スクリプトも一緒に管理し、設計書管理による乖離を解消する

以下のサンプルでは段階的に利用するパッケージを増やし、各パッケージの理解を深める。そうすることで利用するパッケージの差し替えを可能にすることを狙っている。

はじめに
--------

DB を用意する。本記事ではどの環境でも動くことを前提とするためにJava DBを使ってサンプルを作成する

```java:JavaDBSample.java
import java.sql.*;

class JavaDBSample {
    public static void main(String[] args) throws SQLException {
        Connection conn = DriverManager.getConnection(
            "jdbc:derby:/tmp/javadb/sample;create=true");

        conn.close();
    }
}
```

以下により、/tmp/javadb/sample ディレクトリが作成されその配下にDB関連のファイルができる

    javac JavaDBSample.java
    java -classpath "$JAVA_HOME/db/lib/derby.jar;." JavaDBSample

SQL発行までを試す

```java:JavaDBSample.java
import java.sql.*;

class JavaDBSample {
    public static void main(String[] args) throws SQLException {
        Connection conn = DriverManager.getConnection(
            "jdbc:derby:/tmp/javadb/sample;create=true");

        try {
            drop(conn, "sample");

            Statement stat = conn.createStatement();
            stat.execute("create table sample (id numeric(3), str varchar(5))");
            stat.close();

            conn.setAutoCommit(false);
            stat = conn.createStatement();
            stat.execute("insert into sample values (1, 'a')");
            stat.close();

            stat = conn.createStatement();
            ResultSet rs = stat.executeQuery("select * from sample");
            while (rs.next()) {
              System.out.println(rs.getInt("ID") + ", " + rs.getString("STR"));
            }
            rs.close();
            stat.close();
            conn.commit();

            conn.close();
        }
        finally {
            try {
                shutdown();
            }
            catch (SQLException e) {
                // ignore
            }
        }
    }

    public static void drop(Connection conn, String table) {
        try {
            Statement stat = conn.createStatement();
            stat.execute("drop table " + table);
            stat.close();
        }
        catch (SQLException e) {
            // ignore
        }
    }
    public static void shutdown() throws SQLException {
        DriverManager.getConnection("jdbc:derby:;shutdown=true");
    }

}
```

コンパイルする

    javac JavaDBSample.java

実行する

    java -classpath "$JAVA_HOME/db/lib/derby.jar;." JavaDBSample

```
1, a
```


Gradle でビルドする
-------------------

以降、依存ライブラリの解決に gradle を利用するので、gradleをインストールしておく

[Gradle](http://www.gradle.org/)

Gradle は現在 2.0 になっているが、ここでは自分の環境にインストール済みの 1.12 で試した結果を記載する

    gradle -version

```
------------------------------------------------------------
Gradle 1.12
------------------------------------------------------------

Build time:   2014-04-29 09:24:31 UTC
Build number: none
Revision:     a831fa866d46cbee94e61a09af15f9dd95987421

Groovy:       1.8.6
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
Ivy:          2.2.0
JVM:          1.7.0_40 (Oracle Corporation 24.0-b56)
OS:           Windows 7 6.1 x86
```

Gradleで先のソースをビルドするため、以下のファイルを作成する。

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

JavaDBSample.java を以下のパスに移動する
(Gradleではソースツリーの構成が定められている)

    src/main/java/JavaDBSample.java

前のサンプルで作成していた余分な .class を削除しておく

    rm *.class

コンパイルする

    gradle build

実行する。混乱を避けるために前に作ったDBは一旦消しておく

    rm -rf /tmp/javadb/sample

ビルドされたclass ファイルは build/classes/main 配下に作成されるためクラスパスを変更していることに注意。

    java -classpath "$JAVA_HOME/db/lib/derby.jar;build/classes/main" JavaDBSample

```
1, a
```

Spring を使う
-------------

[参考:Spring環境におけるDBアクセス](http://dev.classmethod.jp/server-side/java/db-access-in-spring-jdbctemplate/)

必要な spring の jar は gradle にダウンロードさせる

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

SpringのapplicationContext.xmlを以下の通り作成する。
今度は、一般的なサンプルに合わせ、パッケージを付けることにする。
パッケージ名は、`jp.sample` とした。

DBアクセス周りは慣習(?)に合わせ、META-INF/datasource-context.xml
に記述し、importしている。

```xml:src/main/resources/applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 
  <context:annotation-config />
  <context:component-scan base-package="jp.sample" />
  <import resource="META-INF/datasource-context.xml" />
</beans>
```

META-INF/datasource-context.xml
の設定値は、JavaDBを使うため、以下の通りとした。

|設定項目 |値 |
|-------|-------|
|driverClassName | org.apache.derby.jdbc.EmbeddedDriver |
|url    | jdbc:derby:/tmp/javadb/sample;create=true|
|jdbcTemplate の databaseProductName   | Derby   |

databaseProductName はなくても良いかもしれない。
(databaseProductName を "Apache Derby" と書いていたが、実際は "Derby" だった。[sql-error-codes.xml](https://github.com/spring-projects/spring-framework/blob/master/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml)を参照)
username, password は、一般的なDBに合わせ設定しているが、ここで作成しているJavaDBでは実際は関係ない。

```xml:src/main/resources/META-INF/datasource-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">
 
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="org.apache.derby.jdbc.EmbeddedDriver" />
    <property name="url" value="jdbc:derby:/tmp/javadb/sample;create=true" />
    <property name="username" value="user" />
    <property name="password" value="pass" />
  </bean>
 
  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
  </bean>
 
  <tx:annotation-driven transaction-manager="transactionManager" />
 
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource" />
    <property name="databaseProductName" value="Apache Derby" />
  </bean>
</beans>
```

jp.sample.JavaDBSample2 クラスを作成する

```java:src/main/java/jp/sample/JavaDBSample2.java
package jp.sample;
 
import java.sql.ResultSet;
import java.sql.SQLException;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
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 JavaDBSample2 {
   
  public static void main(String[] args) {
    try (ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml")) {
      JavaDBSample2 sample = context.getBean(JavaDBSample2.class);
      sample.execute();
    }
  }
   
  @Autowired
  JdbcTemplate jdbcTemplate;
   
  @Transactional
  public void execute() {
    // do some work
  }
}
```

コンパイルする

    gradle build

class ファイルが build/classes/main/jp/sample/ に作成される
実行にあたっては、ダウンロードしたjarファイルにクラスパスを通すなど
面倒くさいので実行もgradleで行うことにする。

build.gradle を以下のとおり書き換える

なお、$JAVA_HOME/db/lib/derby.jar にクラスパスを渡すのも
面倒くさかったので、javaDB である derby.jar もダウンロード
するように変更した。

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

mainClassName = 'jp.sample.JavaDBSample2'

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.14.+' // 以下の2019/5/30 追記参照
}
```

※ 2019/5/30 追記: javaDB(Apache Derby)のEmbeddedDriverは、直近のversion 10.15.1.3 で、derbytools.jarに移動した。またこのバージョンではJDK9を必要とする。本記事ではJDK8を対象としたいため10.14を指定することにした ([Apache Derby 10.15.1.3 Release](https://db.apache.org/derby/releases/release-10.15.1.3.cgi))

実行する。前に作ったDBは一旦消す

    rm -rf /tmp/javadb/sample

    gradle run

クエリを実行する
JavaDBSample と同じようなcreate table, insert, select をJdbcTemplateで実行する

ID は、BigDecimal になってしまった。RowMapperを使うようにしないとダメなのか？

```java:src/main/java/jp/sample/JavaDBSample2.java
package jp.sample;
 
import java.sql.ResultSet;
import java.sql.SQLException;

import java.util.List;
import java.util.Map;
import java.math.BigDecimal;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
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 JavaDBSample2 {
   
  public static void main(String[] args) {
    try (ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml")) {
      JavaDBSample2 sample = context.getBean(JavaDBSample2.class);
      sample.execute();
    }
  }
   
  @Autowired
  JdbcTemplate jdbcTemplate;
   
  @Transactional
  public void execute() {
      jdbcTemplate.execute("create table sample2 (id numeric(3), str varchar(5))");
      jdbcTemplate.execute("insert into sample2 values (2, 'b')");
      List<Map<String,Object>> rows = jdbcTemplate.queryForList("select * from sample2");

      for (Map row : rows) {
          System.out.println((BigDecimal)row.get("ID") + ", " + (String)row.get("STR"));
      }
  }
}
```

JUnitで動作確認をする
-------------

この記事の目的はテストなので、JUnitでテストを作成し、結果の正しさ
(ここでは、レコード(2, 'b')が作られること)を確認してみる

Spring でJUnitのテストを書くには、それ用のライブラリ(spring-test)が必要になるようなので、JUnit と一緒にGradleで取ってくることにする。

[@RunWithと@ContextConfigurationでspring用のjunitを作成する](http://javatechnology.net/spring/runwith-junit/)

また、実行は必要なくなるので、plugin は application から　java に戻すことにする

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

また、テストクラスを以下のように作成する

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

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.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
public class JavaDBSample2Test {
 
    @Test
    public void test() {
        assertTrue(true);
    }
}
```

    gradle check

なお、コンパイルエラーではなくテストが実行され失敗した場合には、テストの結果が以下に書かれるので参照する

    build/reports/tests/index.html
次に、ライブラリの不足等ないことを確認したらちゃんとテストを書いてみる