1. 序章

このチュートリアルでは、Java。テストリレーショナルデータベースの相互作用に使用される単体テストツールであるDBUnitを見ていきます。

データベースを既知の状態にし、予想される状態に対してアサートするのにどのように役立つかを見ていきます。

2. 依存関係

まず、dbunit依存関係をpom.xmlに追加することで、MavenCentralからプロジェクトにDBUnitを追加できます。

<dependency>
  <groupId>org.dbunit</groupId>
  <artifactId>dbunit</artifactId>
  <version>2.7.0</version>
  <scope>test</scope>
</dependency>

MavenCentralで最新バージョンを検索できます。

3. HelloWorldの例

次に、データベーススキーマを定義しましょう:

schema.sql

CREATE TABLE IF NOT EXISTS CLIENTS
(
    `id`         int AUTO_INCREMENT NOT NULL,
    `first_name` varchar(100)       NOT NULL,
    `last_name`  varchar(100)       NOT NULL,
    PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS ITEMS
(
    `id`       int AUTO_INCREMENT NOT NULL,
    `title`    varchar(100)       NOT NULL,
    `produced` date,
    `price`    float,
    PRIMARY KEY (`id`)
);

3.1. 初期データベースコンテンツの定義

DBUnitを使用すると、テストデータセットを簡単な宣言型の方法で定義およびロードできます。

各テーブル行を1つのXML要素で定義します。タグ名はテーブル名であり、属性名と値はそれぞれ列名と値にマップされます。 行データは、複数のテーブルに対して作成できます。 DataSourceBasedDBTestCasegetDataSet()メソッドを実装して、初期データセットを定義する必要があります。ここで、FlatXmlDataSetBuilderを使用してXMLファイルを参照できます。

data.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <CLIENTS id='1' first_name='Charles' last_name='Xavier'/>
    <ITEMS id='1' title='Grey T-Shirt' price='17.99' produced='2019-03-20'/>
    <ITEMS id='2' title='Fitted Hat' price='29.99' produced='2019-03-21'/>
    <ITEMS id='3' title='Backpack' price='54.99' produced='2019-03-22'/>
    <ITEMS id='4' title='Earrings' price='14.99' produced='2019-03-23'/>
    <ITEMS id='5' title='Socks' price='9.99'/>
</dataset>

3.2. データベース接続とスキーマの初期化

スキーマができたので、データベースを初期化する必要があります。

DataSourceBasedDBTestCase クラスを拡張し、 getDataSource()メソッドでデータベーススキーマを初期化する必要があります。

DataSourceDBUnitTest.java

public class DataSourceDBUnitTest extends DataSourceBasedDBTestCase {
    @Override
    protected DataSource getDataSource() {
        JdbcDataSource dataSource = new JdbcDataSource();
        dataSource.setURL(
          "jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;init=runscript from 'classpath:schema.sql'");
        dataSource.setUser("sa");
        dataSource.setPassword("sa");
        return dataSource;
    }

    @Override
    protected IDataSet getDataSet() throws Exception {
        return new FlatXmlDataSetBuilder().build(getClass().getClassLoader()
          .getResourceAsStream("data.xml"));
    }
}

ここでは、SQLファイルを接続文字列でH2インメモリデータベースに渡しました。 他のデータベースでテストする場合は、そのカスタム実装を提供する必要があります。

この例では、 DBUnitは、各テストメソッドの実行の前に、指定されたテストデータでデータベースを再初期化することに注意してください。

get SetUpOperationおよびget TearDownOperationを介してこれを構成する方法は複数あります。

@Override
protected DatabaseOperation getSetUpOperation() {
    return DatabaseOperation.REFRESH;
}

@Override
protected DatabaseOperation getTearDownOperation() {
    return DatabaseOperation.DELETE_ALL;
}

REFRESH 操作は、DBUnitにすべてのデータを更新するように指示します。 これにより、すべてのキャッシュがクリアされ、単体テストが別の単体テストの影響を受けないことが保証されます。 DELETE_ALL 操作は、各単体テストの終了時にすべてのデータが削除されることを保証します。 この例では、セットアップ中に getSetUpOperation メソッドの実装を使用して、すべてのキャッシュを更新することをDBUnitに通知しています。 最後に、 getTearDownOperation メソッドの実装を使用して、ティアダウン操作中にすべてのデータを削除するようにDBUnitに指示します。

3.3. 期待される状態と実際の状態の比較

それでは、実際のテストケースを見てみましょう。 この最初のテストでは、単純に保ちます。予想されるデータセットをロードし、DB接続から取得したデータセットと比較します。

@Test
public void givenDataSetEmptySchema_whenDataSetCreated_thenTablesAreEqual() throws Exception {
    IDataSet expectedDataSet = getDataSet();
    ITable expectedTable = expectedDataSet.getTable("CLIENTS");
    IDataSet databaseDataSet = getConnection().createDataSet();
    ITable actualTable = databaseDataSet.getTable("CLIENTS");
    assertEquals(expectedTable, actualTable);
}

4. アサーションの詳細

前のセクションでは、テーブルの実際の内容を予想されるデータセットと比較する基本的な例を見ました。 次に、データアサーションをカスタマイズするためのDBUnitのサポートを確認します。

4.1. SQLクエリによるアサート

実際の状態を確認する簡単な方法は、SQLクエリを使用することです。

この例では、CLIENTSテーブルに新しいレコードを挿入してから、新しく作成された行の内容を確認します。 別のXMLファイルで期待される出力を定義し、SQLクエリによって実際の行の値を抽出しました。

@Test
public void givenDataSet_whenInsert_thenTableHasNewClient() throws Exception {
    try (InputStream is = getClass().getClassLoader().getResourceAsStream("dbunit/expected-user.xml")) {
        IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
        ITable expectedTable = expectedDataSet.getTable("CLIENTS");
        Connection conn = getDataSource().getConnection();

        conn.createStatement()
            .executeUpdate(
            "INSERT INTO CLIENTS (first_name, last_name) VALUES ('John', 'Jansen')");
        ITable actualData = getConnection()
            .createQueryTable(
                "result_name",
                "SELECT * FROM CLIENTS WHERE last_name='Jansen'");

        assertEqualsIgnoreCols(expectedTable, actualData, new String[] { "id" });
    }
}

DBTestCase祖先クラスのgetConnection()メソッドは、データソース接続( IDatabaseConnection インスタンス)のDBUnit固有の表現を返します。 IDatabaseConnectionのcreateQueryTable()メソッドを使用して、 Assertion.assertEquals()メソッドを使用して、予想されるデータベースの状態と比較するために、データベースから実際のデータをフェッチできます。 createQueryTable()に渡されるSQLクエリは、テストするクエリです。 これは、アサートを行うために使用するTableインスタンスを返します。

4.2. 列を無視する

データベーステストでは、実際のテーブルの一部の列を無視したい場合があります。 これらは通常、生成された主キーや現在のタイムスタンプのように、厳密に制御できない自動生成された値です。

SQLクエリのSELECT句からcolumnsを省略することでこれを行うことができますが、DBUnitはこれを実現するためのより便利なユーティリティを提供します。 DefaultColumnFilterクラスの静的メソッドを使用すると、次に示すように、一部の列を除外することで、既存のインスタンスから新しいITableインスタンスを作成できます。

@Test
public void givenDataSet_whenInsert_thenGetResultsAreStillEqualIfIgnoringColumnsWithDifferentProduced()
  throws Exception {
    Connection connection = tester.getConnection().getConnection();
    String[] excludedColumns = { "id", "produced" };
    try (InputStream is = getClass().getClassLoader()
      .getResourceAsStream("dbunit/expected-ignoring-registered_at.xml")) {
        IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
        ITable expectedTable = excludedColumnsTable(expectedDataSet.getTable("ITEMS"), excludedColumns);

        connection.createStatement()
          .executeUpdate("INSERT INTO ITEMS (title, price, produced)  VALUES('Necklace', 199.99, now())");

        IDataSet databaseDataSet = tester.getConnection().createDataSet();
        ITable actualTable = excludedColumnsTable(databaseDataSet.getTable("ITEMS"), excludedColumns);

        assertEquals(expectedTable, actualTable);
    }
}

4.3. 複数の障害の調査

DBUnitの場合不正な値が見つかると、すぐにAssertionErrorがスローされます。

特定のケースでは、 DiffCollectingFailureHandler クラスを使用できます。このクラスは、3番目の引数として Assertion.assertEquals()メソッドに渡すことができます。

この障害ハンドラーは、最初の障害で停止するのではなく、すべての障害を収集します。つまり、 Assertion.assertEquals()メソッドは、 DiffCollectingFailureHandler。 したがって、ハンドラーがエラーを検出したかどうかをプログラムで確認する必要があります。

@Test
public void givenDataSet_whenInsertUnexpectedData_thenFailOnAllUnexpectedValues() throws Exception {
    try (InputStream is = getClass().getClassLoader()
      .getResourceAsStream("dbunit/expected-multiple-failures.xml")) {
        IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
        ITable expectedTable = expectedDataSet.getTable("ITEMS");
        Connection conn = getDataSource().getConnection();
        DiffCollectingFailureHandler collectingHandler = new DiffCollectingFailureHandler();

        conn.createStatement()
          .executeUpdate("INSERT INTO ITEMS (title, price) VALUES ('Battery', '1000000')");
        ITable actualData = getConnection().createDataSet().getTable("ITEMS");

        assertEquals(expectedTable, actualData, collectingHandler);
        if (!collectingHandler.getDiffList().isEmpty()) {
            String message = (String) collectingHandler.getDiffList()
                .stream()
                .map(d -> formatDifference((Difference) d))
                .collect(joining("\n"));
            logger.error(() -> message);
        }
    }
}

private static String formatDifference(Difference diff) {
    return "expected value in " + diff.getExpectedTable()
      .getTableMetaData()
      .getTableName() + "." + 
      diff.getColumnName() + " row " + 
      diff.getRowIndex() + ":" + 
      diff.getExpectedValue() + ", but was: " + 
      diff.getActualValue();
}

さらに、ハンドラーは Difference インスタンスの形式で失敗を提供します。これにより、エラーをフォーマットできます。

テストを実行した後、フォーマットされたレポートを取得します。

java.lang.AssertionError: expected value in ITEMS.price row 5:199.99, but was: 1000000.0
expected value in ITEMS.produced row 5:2019-03-23, but was: null
expected value in ITEMS.title row 5:Necklace, but was: Battery

	at com.baeldung.dbunit.DataSourceDBUnitTest.givenDataSet_whenInsertUnexpectedData_thenFailOnAllUnexpectedValues(DataSourceDBUnitTest.java:91)

この時点で、新しいアイテムの価格は199.99であると予想していましたが、1000000.0であったことに注意してください。 すると、製造日は2019-03-23であることがわかりますが、最終的にはnullでした。 最後に、期待されたアイテムはネックレスでした、そして代わりに我々はバッテリーを手に入れました。

5. 結論

この記事では、DBUnitがJavaアプリケーションのテストデータからテストデータアクセスレイヤーを定義する宣言型の方法をどのように提供するかを見ました。

いつものように、例の完全なソースコードは、GitHubから入手できます。