Java
assertj
dbsetup

DbsetupとAssertJ-DBでDBのテスト

More than 1 year has passed since last update.


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