LoginSignup
40

More than 5 years have passed since last update.

DbsetupとAssertJ-DBでDBのテスト

Last updated at Posted at 2015-12-26

JavaでDBのテスト

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

DBUnit

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

本家サイトから抜粋: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用のテストデータセットアップライブラリ。
本家サイト
qiitaの記事
ブログ:本家の翻訳

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

本家サイトから抜粋
 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を利用する。

本家サイトから抜粋
 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
本家サイト:Assertj-DB
Assertjは、「流れるようなインタフェース(Fluent Interface)」でアサーションが書ける。
http://qiita.com/ikemo/items/165f01740995245f9009

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

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

本家サンプル

やってみての感想:さよなら 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)として実行する。
本家サイト:アサーションジェネレータ
本家のbuild.gradleの例

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)

テスト対象のクラス

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;
    }
}
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を利用。

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");

    }

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

    }
}

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

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


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
40