DbsetupとAssertJ-DBでDBのテスト

#JavaでDBのテスト
　実際のテストでめんどいDBのテストデータの準備とDBの状態のテスト。これまではDBUnitしか使ったことなかったけど、Dbsetupでテストデータを準備し、AssertJ,AssertJ-DBでDBのテストをやってみた。

##DBUnit
 http://dbunit.sourceforge.net/intro.html
　データを外部ファイルで管理して、Assertionも期待値を格納したファイルと比較して書ける。

```java:本家サイトから抜粋：DBから抽出したデータと外部ファイルのデータを比較してAssertion
  // Fetch database data after executing your code
        IDataSet databaseDataSet = getConnection().createDataSet();
        ITable actualTable = databaseDataSet.getTable("TABLE_NAME");

        // Load expected data from an XML dataset
        IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(new File("expectedDataSet.xml"));
        ITable expectedTable = expectedDataSet.getTable("TABLE_NAME");

        // Assert actual database table match expected table
        Assertion.assertEquals(expectedTable, actualTable);

```
 ただ、小さなデータをセットアップするのに、いちいちファイル用意するのも辛いし、アサーションもテーブル全体（じゃなければSELECT結果）を比較しなきゃならないので、でかいテーブルなど、中々辛い。

##Dbsetup
外部ファイルを利用しないJava用のテストデータセットアップライブラリ。
[本家サイト](http://dbsetup.ninja-squad.com/)
[qiitaの記事](http://qiita.com/ikemo/items/a27ece72fe2af3683ff6)
[ブログ：本家の翻訳](http://kagamihoge.hatenablog.com/entry/2015/08/14/205330)

データは「流れるようなインタフェース（Fluent Interface）」で作成する。

```java:本家サイトから抜粋
 public static final Operation INSERT_REFERENCE_DATA =
        sequenceOf(
            insertInto("COUNTRY")
                .columns("ID", "ISO_CODE", "NAME")
                .values(1, "FRA", "France")
                .values(2, "USA", "United States")
                .build(),
            insertInto("USER")
                .columns("ID", "LOGIN", "NAME")
                .values(1L, "jbnizet", "Jean-Baptiste Nizet")
                .values(2L, "clacote", "Cyril Lacote")
                .build());

```

セットアップの実行は@Beforeメソッド内でDbSetupTrackerを利用する。

```java:本家サイトから抜粋
 private static DbSetupTracker dbSetupTracker = new DbSetupTracker();
 
　//@Beforeメソッドで下記のような処理  
        // same DbSetup definition as above
        DbSetup dbSetup = new DbSetup(new DataSourceDestination(dataSource), operation);
        
        // セットアップの実行
        dbSetupTracker.launchIfNecessary(dbSetup);
```

参照系のテストメソッドで、dbSetupTracker.skipNextLaunch();を呼び出すと、次のテストメソッド実行時に、セットアップ実行がスキップされる。

#AssertjとAssertj-DB

[本家サイト:Assertj](http://joel-costigliola.github.io/assertj/)
[本家サイト:Assertj-DB](http://joel-costigliola.github.io/assertj/assertj-db.html)
Assertjは、「流れるようなインタフェース（Fluent Interface）」でアサーションが書ける。
http://qiita.com/ikemo/items/165f01740995245f9009

Assertj-DBは結構最近できたみたい。「流れるようなインタフェース（Fluent Interface）」でDBをアサートできる。

ちょっと、考え方と使い方に慣れが必要だけど、Changeという差分を抽出してくれるものがあって、Unitテストとかには結構使えそう。

[本家サンプル](https://github.com/joel-costigliola/assertj-examples/tree/master/assertions-examples/src/test/java/org/assertj/examples/db)

#やってみての感想：さよなら DBUnit
- Dbsetup
プログラミングする立場からは、絶対にDBUnitより、Dbsetupの方が使いやすい。証跡みたいなのが必要がな現場では、セットアップデータをエクスポートするTestRuleみたいのを作ればいいんじゃないかな。

- Assertj
使わない理由0。使おう。

- Assertj-DB
少なくともUnitテストレベルだと問題なく使えそう。慣れが必要だけど、慣れればかなり生産性は上がる気がする。カラム数が多い複雑なテーブルとかでもOKと思う。まあ、DBのテストで生産性と一番関係あるのはツールよりテーブル設計なんだけどね。。。
軽く動作確認したら、H2DBでは動くけど、sqliteではPKをうまく認識できないなどまだまだ、バグは多そう。
→2016.03.12現在で、バグは修正。報告してから3か月位。関連した機能改善と一緒にだけど、まあ、個人で頑張ってもらっているみたい。

まあ、ということで、結論は1年後にはさよならDBUnit

#やってみたやつ
##Gradle
sample.Personはgetter,setterのみのBeanクラス。Beanプロパティをテストするアサーションを自動生成するmavenプラグインをAssertJは提供している。下記のbuild.gradleにはgradleのタスク（assertjGen）として実行する。
[本家サイト:アサーションジェネレータ](http://joel-costigliola.github.io/assertj/assertj-assertions-generator.html)
[本家のbuild.gradleの例](https://github.com/joel-costigliola/assertj-assertions-generator/blob/master/src/main/scripts/build.gradle)

```groovy:build.gradle

version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


configurations {
    assertj
}

dependencies {
    compile 'com.h2database:h2:1.4.190'
    testCompile 'junit:junit:4.11'
    testCompile 'org.assertj:assertj-core:3.2.0'
    testCompile 'org.assertj:assertj-db:1.0.1'
    testCompile 'com.ninja-squad:DbSetup:1.6.0'

    assertj 'org.assertj:assertj-assertions-generator:2.0.0'
    assertj project
}


sourceSets {
    test {
        java {
            srcDir 'src/test/java'
            srcDir 'src-gen/test/java'
        }
    }
}


def assertjOutput = file('src-gen/test/java')

task assertjClean(type: Delete) {
    delete assertjOutput
}

task assertjGen(dependsOn: assertjClean, type: JavaExec) {
    doFirst {
        if (!assertjOutput.exists()) {
            logger.info("Creating `$assertjOutput` directory")

            if (!assertjOutput.mkdirs()) {
                throw new InvalidUserDataException("Unable to create `$assertjOutput` directory")
            }
        }
    }

    main 'org.assertj.assertions.generator.cli.AssertionGeneratorLauncher'
    classpath = files(configurations.assertj)
    workingDir = assertjOutput
    args = ['sample.Person'
    ]
}


compileTestJava.dependsOn(assertjGen)
```

##テスト対象のクラス

```java:Person.java
package sample;

public class Person {

    private int id;
    private String name;
    private int age;

    public Person(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
```

```java:PersonDao
package sample;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class PersonDao {

    private static final String DELETE = "DELETE FROM person WHERE id = ?" ;
    private Connection con;
    private static final String SELECT = "SELECT id,name,age FROM person ";
    private static final String ORDER_BY_ID = "ORDER BY id ";
    private static final String UPDATE = "UPDATE person SET name = ?, age = ? WHERE id =?";

    public PersonDao(Connection con) {
        this.con = con;
    }

    public Person findById(int id) {

        Person result = null;

        String sql = SELECT + "WHERE id = ? " + ORDER_BY_ID;
        try (PreparedStatement pst = con.prepareStatement(sql)) {
            pst.setInt(1, id);
            ResultSet rs = pst.executeQuery();
            if (rs.next()) {
                result = createPersonFrom(rs);
            }

        } catch (SQLException e) {

            e.printStackTrace();
        }


        return result;
    }

    public List<Person> findAll() {
        List<Person> result = new ArrayList<>();

        String sql = SELECT;
        try (PreparedStatement pst = con.prepareStatement(sql)) {
            ResultSet rs = pst.executeQuery();
            while (rs.next()) {
                result.add(createPersonFrom(rs));
            }

        } catch (SQLException e) {
            e.printStackTrace();
        }

        return result;
    }

    public int update(Person p) {
        int result = 0;
        String sql = UPDATE;
        try (PreparedStatement pst = con.prepareStatement(sql)) {
            pst.setString(1, p.getName());
            pst.setInt(2, p.getAge());
            pst.setInt(3, p.getId());
            result = pst.executeUpdate();

        } catch (SQLException e) {
            e.printStackTrace();
        }


        return result;
    }

    public int delete(int id) {
        int result = 0;
        String sql = DELETE;
        try (PreparedStatement pst = con.prepareStatement(sql)) {
            pst.setInt(1, id);
            result = pst.executeUpdate();

        } catch (SQLException e) {
            e.printStackTrace();
        }


        return result;
    }


    private Person createPersonFrom(ResultSet rs) throws SQLException {
        return new Person(rs.getInt("id"), rs.getString("name"), rs.getInt("age"));
    }
}

```

##テストクラス

生成したPesonAssertを利用。

```java:PesonTest
package sample;

import org.junit.Test;

import static sample.PersonAssert.*;

public class PersonTest {


    @Test
    public void test() throws Exception {

        Person p = new Person(1,"Bob", 20);
        assertThat(p)
                .hasId(1)
                .hasAge(20)
                .hasName("Bob");

    }

}
```

```java:PersonDaoTest

package sample;

import com.ninja_squad.dbsetup.DbSetup;
import com.ninja_squad.dbsetup.DbSetupTracker;
import com.ninja_squad.dbsetup.Operations;
import com.ninja_squad.dbsetup.destination.Destination;
import com.ninja_squad.dbsetup.destination.DriverManagerDestination;
import com.ninja_squad.dbsetup.operation.Operation;
import org.assertj.db.type.Changes;
import org.assertj.db.type.Source;
import org.junit.Before;
import org.junit.Test;

import java.sql.Connection;
import java.util.List;

import static sample.PersonAssert.*;
import static org.assertj.core.api.Assertions.*;

import static org.assertj.db.api.Assertions.*;

public class PersonDaoTest {

    private static final String url = "jdbc:h2:./sample";
    private static final Destination dest = new DriverManagerDestination(url, "sa", "");
    private  static final Source SOURCE = new Source(url, "sa", "");
    private static DbSetupTracker dbSetupTracker = new DbSetupTracker();

    private static final Operation DELETE_ALL_PERSON = Operations.deleteAllFrom("person");
    private static final Operation INSERT_PERSON = Operations.insertInto("person")
            .columns("id", "name", "age")
             .values(1, "Bob", 10)
             .values(2, "John", 20)
            .build();

    @Before
    public void before() {
        DbSetup setup = new DbSetup(dest, Operations.sequenceOf(DELETE_ALL_PERSON, INSERT_PERSON));
        dbSetupTracker.launchIfNecessary(setup);
    }

    @Test
    public void testFindByName() throws Exception {
        dbSetupTracker.skipNextLaunch();
        try (Connection con = dest.getConnection()) {
            PersonDao dao = new PersonDao(con);

            Person p = dao.findById(1);

            assertThat(p)
                    .isNotNull()
                    .hasId(1)
                    .hasName("Bob")
                    .hasAge(10);
        }
    }

    @Test
    public void testFindAll() throws Exception {
        dbSetupTracker.skipNextLaunch();
        try (Connection con = dest.getConnection()) {
            PersonDao dao = new PersonDao(con);

            List<Person> pList = dao.findAll();
            assertThat(pList)
                    .isNotNull()
                    .isNotEmpty()
                    .extracting("id", "name", "age")//プロパティ名を文字列で指定
                    .contains(tuple(1, "Bob", 10), atIndex(0))
                    .contains(tuple(2, "John", 20), atIndex(1));

        }

    }

    @Test
    public void testUpdate() throws Exception {

        Changes changes = new Changes(SOURCE);
        changes.setStartPointNow();

        try (Connection con = dest.getConnection()) {
            PersonDao dao = new PersonDao(con);
            Person modifyPerson = new Person(1, "Sam", 30);
            int result = dao.update(modifyPerson);

            assertThat(result)
                    .isEqualTo(1);
        }

        changes.setEndPointNow();
        assertThat(changes)
                .hasNumberOfChanges(1)
                .change()
                 .isModification()
                 .isOnTable("person")
                 .hasPksValues(1)
                 .rowAtStartPoint()
                  .hasValues(1,"Bob",10)
                 .rowAtEndPoint()
                  .hasValues(1,"Sam",30);

    }

    @Test
    public void testDelete() throws Exception {
        Changes changes = new Changes(SOURCE);
        changes.setStartPointNow();

        try (Connection con = dest.getConnection()) {
            PersonDao dao = new PersonDao(con);
            dao.delete(1);
        }

        changes.setEndPointNow();
        assertThat(changes)
                .hasNumberOfChanges(1)
                .change()
                 .isDeletion()
                 .hasPksValues(1);

    }
}
```

テーブルセットアップ用クラス

```java:DbSetup
package sample;

import com.ninja_squad.dbsetup.destination.DriverManagerDestination;
import org.junit.Ignore;
import org.junit.Test;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;


public class DbSetup {

    @Test
    @Ignore
    public void setupTestDb(){
        try(Connection con = new DriverManagerDestination("jdbc:h2:./sample", "sa", "").getConnection()){

            Statement stm = con.createStatement();
            stm.execute("CREATE  TABLE person(id int PRIMARY KEY ,name VARCHAR(10),age int )");
            stm.close();

        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}


```