#JavaでDBのテスト
実際のテストでめんどいDBのテストデータの準備とDBの状態のテスト。これまではDBUnitしか使ったことなかったけど、Dbsetupでテストデータを準備し、AssertJ,AssertJ-DBでDBのテストをやってみた。
##DBUnit
http://dbunit.sourceforge.net/intro.html
データを外部ファイルで管理して、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の例
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)
##テスト対象のクラス
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;
}
}
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を利用。
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");
}
}
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);
}
}
テーブルセットアップ用クラス
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();
}
}
}