読者です 読者をやめる 読者になる 読者になる

かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( 番外編 )( テスト用クラスの機能追加・変更 )

概要

作成していたテスト用クラスに新しい機能が欲しくなったり、変更したい点が出来たので、今回対応します。

  • DB処理不要のアノテーションが付加されたテストメソッドではバックアップ&リストア&テストデータロードの処理を実行しないようにする機能を TestDataResource クラスに追加する。
  • TestDataResource クラスでバックアップを取得するテーブルをクラス内に列挙するのではなく TESTDATA_BASE_DIR で指定したディレクトリ内の table-ordering.txt から読み込むようにする。
  • TableDataAssert クラスに比較するカラムだけを指定するメソッドを追加する。
  • TestHelper クラスに Errors インターフェースの実装クラスのオブジェクトを生成するメソッドを追加する。
  • TestHelper クラスから assertEntityByForm メソッドを削除する。
  • TestDataLoader クラスの catch 内の処理を e.printStackTrace() → throw new RuntimeException(e) に変更する。
  • ksbysample.common.test の下にサブパッケージを作成してファイルを配置し直す。

参照したサイト・書籍

  1. enum の比較は安心して == を使ってください
    http://etc9.hatenablog.com/entry/2013/10/03/092927

    • enum の比較を == でやってよいのか分からなかったので調査した時に参照しました。

目次

  1. DB処理不要のアノテーションが付加されたテストメソッドではバックアップ&リストア&テストデータロードの処理を実行しないようにする機能を TestDataResource クラスに追加する
  2. TestDataResource クラスでバックアップを取得するテーブルをクラス内に列挙するのではなく TESTDATA_BASE_DIR で指定したディレクトリ内の table-ordering.txt から読み込むようにする
  3. TableDataAssert クラスに比較するカラムだけを指定するメソッドを追加する
  4. TestHelper クラスに Errors クラスのインスタンスを生成するメソッドを追加する
  5. TestHelper クラスから assertEntityByForm メソッドを削除する
  6. TestDataLoader クラスの catch 内の処理を e.printStackTrace() → throw new RuntimeException(e) に変更する
  7. ksbysample.common.test の下にサブパッケージを作成してファイルを配置し直す

手順

DB処理不要のアノテーションが付加されたテストメソッドではバックアップ&リストア&テストデータロードの処理を実行しないようにする機能を TestDataResource クラスに追加する

テストクラス内に TestDataResource クラスを記述すると DB のバックアップ&リストア処理が実行されるため、これまではバックアップ&リストア処理が必要なテストと不要なテストでテストクラスを分けるようにしていました。

最近テストクラスを分けて書くのがちょっと面倒に思えてきたので、バックアップ&レストア処理が不要なテストメソッドに専用のアノテーションを付加しておけば処理が実行されないようにします。

  1. feature/61-issue ブランチを作成します。

  2. DB処理不要のアノテーションを作成します。src/test/java/ksbysample/common/test の下に NoUseTestDataResource.java を作成します。作成後、リンク先の内容 に変更します。

  3. src/test/java/ksbysample/common/test の下の TestDataResource.javaリンク先のその1の内容 に変更します。

  4. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して "BUILD SUCCESSFUL" のメッセージが表示されることを確認します。

    f:id:ksby:20160123103915p:plain

  5. Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20160123104210p:plain

  6. commit、GitHub へ Push、feature/61-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/61-issue ブランチを削除、をします。

TestDataResource クラスでバックアップを取得するテーブルをクラス内に列挙するのではなく TESTDATA_BASE_DIR で指定したディレクトリ内の table-ordering.txt から読み込むようにする

バックアップ対象のテーブルを TestDataResource クラス内の BACKUP_TABLES に定義しているのですが、TESTDATA_BASE_DIR に記述したディレクトリ内に用意したデータファイルのテーブルだけバックアップしてくれれば十分なので、TESTDATA_BASE_DIR に記述したディレクトリ内にある table-ordering.txt からバックアップ対象のテーブルを読み込んでバックアップするように変更します。

  1. feature/65-issue ブランチを作成します。

  2. src/test/java/ksbysample/common/test の下の TestDataResource.javaリンク先のその2の内容 に変更します。

  3. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して "BUILD SUCCESSFUL" のメッセージが表示されることを確認します。

    f:id:ksby:20160123145643p:plain

  4. Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20160123145946p:plain

  5. commit、GitHub へ Push、feature/65-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/65-issue ブランチを削除、をします。

TableDataAssert クラスに比較するカラムだけを指定するメソッドを追加する

TableDataAssert クラスの assertEquals メソッドCSV ファイルとテーブルのデータを比較する時に第2引数に比較しないカラムのリストを指定できるようにしていたのですが、比較するカラムの数が少なく除外するカラムが多い時には指定するのが手間でした。

TableDataAssert クラスの中で使用している DefaultColumnFilter のメソッドを確認してみると excludedColumnsTable メソッド以外に includedColumnsTable というメソッドも用意されていることが分かりましたので、比較するカラムだけを指定することもできるようにします。

  1. feature/62-issue ブランチを作成します。

  2. src/test/java/ksbysample/common/test の下の AssertOptions.javaリンク先の内容 に変更します。

  3. src/test/java/ksbysample/common/test の下の TableDataAssert.javaリンク先の内容 に変更します。

  4. 作成済のテストで比較するカラムだけを指定するメソッドを使うように修正して動作を確認します。src/test/java/ksbysample/webapp/lending/web/lendingapproval の下の LendingapprovalControllerTest.javaリンク先の内容 に変更します。

  5. テストメソッドを実行します。テストメソッドの左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run '確定ボタンをクリックした場合_却下と...()' with Coverage」を選択します。

    テストが成功することが確認できます。

    f:id:ksby:20160123180259p:plain

  6. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して "BUILD SUCCESSFUL" のメッセージが表示されることを確認します。

    f:id:ksby:20160123180638p:plain

  7. Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20160123180935p:plain

  8. commit、GitHub へ Push、feature/62-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/62-issue ブランチを削除、をします。

TestHelper クラスに Errors インターフェースの実装クラスのオブジェクトを生成するメソッドを追加する

FormValidator クラスのテストを作成する時に Errors errors = new MapBindingResult(new HashMap<String, String>(), ""); と記述して Errors インターフェースの実装クラスのオブジェクトを生成していたのですが、毎回なんか覚えていなくて以前作成したテストを見直してコピーしていたので TestHelper クラスにメソッドを作成して簡単に作成できるようにします。

  1. feature/59-issue ブランチを作成します。

  2. src/test/java/ksbysample/common/test の下の TestHelper.javaリンク先の内容 に変更します。

  3. Ctrl+Shift+R を押して「Replace in Path」ダイアログを表示し、Project 内の Errors errors = new MapBindingResult(new HashMap<String, String>(), "");Errors errors = TestHelper.createErrors(); へ置換します。

    f:id:ksby:20160123190225p:plain

  4. 置換後 Ctrl+F9 を押して build すると TestHelper クラスを import していないクラスがエラーになるので、エディタで開いた後 Alter+Enter を押して import 文を追加します。

  5. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して "BUILD SUCCESSFUL" のメッセージが表示されることを確認します。

    f:id:ksby:20160123190940p:plain

  6. Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20160123191226p:plain

  7. commit、GitHub へ Push、feature/59-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/59-issue ブランチを削除、をします。

TestHelper クラスから assertEntityByForm メソッドを削除する

最初に TestHelper クラスを作成した時は必要と思って作成した assertEntityByForm メソッドですが、最近は使うこともなくなったのでメソッドを削除します。

  1. feature/60-issue ブランチを作成します。

  2. src/test/java/ksbysample/common/test の下の TestHelper.java から assertEntityByForm メソッドを削除します。今回はソースは書きません。

  3. commit、GitHub へ Push、feature/60-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/60-issue ブランチを削除、をします。

TestDataLoader クラスの catch 内の処理を e.printStackTrace() → throw new RuntimeException(e) に変更する

単にソースを見直していて e.printStackTrace() はないな。。。と思ったので修正します。また TestDataResource クラスを変更した時に実施した内容も反映します。

  1. feature/64-issue ブランチを作成します。

  2. src/test/java/ksbysample/common/test の下の TestDataLoader.javaリンク先の内容 に変更します。

  3. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して "BUILD SUCCESSFUL" のメッセージが表示されること、Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択しテストが全て成功することを確認します。

  4. commit、GitHub へ Push、feature/64-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/64-issue ブランチを削除、をします。

ksbysample.common.test の下にサブパッケージを作成してファイルを配置し直す

テスト用クラスのファイルを ksbysample.common.test の下にサブパッケージも作らずに置いておきましたが、ファイルも増えて分かりにくくなってきたのでサブパッケージを作成して配置し直します。

  1. feature/67-issue ブランチを作成します。

  2. IntelliJ IDEA の Project View 上で以下のディレクトリ構成に変更します。テスト用クラスを利用しているテストクラスは IntelliJ IDEA のリファクタリングの機能で自動的に必要な箇所が書き換わります。

    f:id:ksby:20160123221803p:plain

  3. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して "BUILD SUCCESSFUL" のメッセージが表示されることを確認します。

    f:id:ksby:20160123222308p:plain

  4. Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20160123222719p:plain

  5. commit、GitHub へ Push、feature/67-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/67-issue ブランチを削除、をします。

ソースコード

NoUseTestDataResource.java

package ksbysample.common.test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoUseTestDataResource {
}

TestDataResource.java

■その1

package ksbysample.common.test;

import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

@Component
public class TestDataResource extends TestWatcher {

    private final String TESTDATA_BASE_DIR = "src/test/resources/testdata/base";
    private final String BACKUP_FILE_NAME = "ksbylending_backup";
    private final List<String> BACKUP_TABLES = Arrays.asList(
            "user_info"
            , "user_role"
            , "library_forsearch"
            , "lending_app"
            , "lending_book"
    );

    private final String NULL_STRING = "[null]";
    
    @Autowired
    private DataSource dataSource;

    @Autowired
    private TestDataLoader testDataLoader;
    
    private File backupFile;
    
    @Override
    protected void starting(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NoUseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = new DatabaseConnection(dataSource.getConnection());

                // バックアップを取得する
                backupDb(conn);

                // TESTDATA_BASE_DIR で指定されたディレクトリ内のテストデータをロードする
                testDataLoader.load(TESTDATA_BASE_DIR);

                // テストメソッドに @TestData アノテーションが付加されている場合には、
                // アノテーションで指定されたテストデータをロードする
                loadTestData(description);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {}
        }
    }

    @Override
    protected void finished(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NoUseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = new DatabaseConnection(dataSource.getConnection());

                // バックアップからリストアする
                restoreDb(conn);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {}

            if (backupFile != null) {
                try {
                    Files.delete(backupFile.toPath());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                backupFile = null;
            }
        }
    }

    private boolean hasNoUseTestDataResourceAnnotation(Description description) {
        Collection<Annotation> annotationList = description.getAnnotations();
        boolean result = annotationList.stream()
                .anyMatch(annotation -> annotation instanceof NoUseTestDataResource);
        return result;
    }
    
    private void backupDb(IDatabaseConnection conn)
            throws DataSetException, IOException {
        QueryDataSet partialDataSet = new QueryDataSet(conn);
        for (String backupTable : BACKUP_TABLES) {
            partialDataSet.addTable(backupTable);
        }
        ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
        replacementDatasetBackup.addReplacementObject("", NULL_STRING);
        this.backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
        try (FileOutputStream fos = new FileOutputStream(this.backupFile)) {
            FlatXmlDataSet.write(replacementDatasetBackup, fos);
        }
    }

    private void restoreDb(IDatabaseConnection conn)
            throws MalformedURLException, DatabaseUnitException, SQLException {
        if (this.backupFile != null) {
            IDataSet dataSet = new FlatXmlDataSetBuilder().build(this.backupFile);
            ReplacementDataSet replacementDatasetRestore = new ReplacementDataSet(dataSet);
            replacementDatasetRestore.addReplacementObject(NULL_STRING, null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDatasetRestore);
        }
    }

    private void loadTestData(Description description) {
        description.getAnnotations().stream()
                .filter(annotation -> annotation instanceof TestData)
                .forEach(annotation -> {
                    TestData testData = (TestData)annotation;
                    testDataLoader.load(testData.value());
                });
    }
    
}
  • hasNoUseTestDataResourceAnnotation メソッドを追加し、starting メソッド、finished メソッド内の処理を if (!hasNoUseTestDataResourceAnnotation(description)) { ... } で囲みます。
  • ついでに以下の変更もしています。
    • CSV ファイルのテストデータをロードする処理は TestDataLoader クラスを使用するようにします。
    • starting メソッド、finished メソッド内の処理を別メソッドの切り出して見やすくします。

■その2

package ksbysample.common.test;

import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;

@Component
public class TestDataResource extends TestWatcher {

    private static final String TESTDATA_BASE_DIR = "src/test/resources/testdata/base";
    private static final String BACKUP_FILE_NAME = "ksbylending_backup";
    private static final String NULL_STRING = "[null]";
    
    @Autowired
    private DataSource dataSource;

    @Autowired
    private TestDataLoader testDataLoader;
    
    private File backupFile;
    
    @Override
    protected void starting(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NoUseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = new DatabaseConnection(dataSource.getConnection());

                // バックアップを取得する
                backupDb(conn);

                // TESTDATA_BASE_DIR で指定されたディレクトリ内のテストデータをロードする
                testDataLoader.load(TESTDATA_BASE_DIR);

                // テストメソッドに @TestData アノテーションが付加されている場合には、
                // アノテーションで指定されたテストデータをロードする
                loadTestData(description);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {}
        }
    }

    @Override
    protected void finished(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NoUseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = new DatabaseConnection(dataSource.getConnection());

                // バックアップからリストアする
                restoreDb(conn);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {}

            if (backupFile != null) {
                try {
                    Files.delete(backupFile.toPath());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                backupFile = null;
            }
        }
    }

    private boolean hasNoUseTestDataResourceAnnotation(Description description) {
        Collection<Annotation> annotationList = description.getAnnotations();
        boolean result = annotationList.stream()
                .anyMatch(annotation -> annotation instanceof NoUseTestDataResource);
        return result;
    }
    
    private void backupDb(IDatabaseConnection conn)
            throws DataSetException, IOException {
        QueryDataSet partialDataSet = new QueryDataSet(conn);

        // TESTDATA_BASE_DIR で指定されたディレクトリ内の table-ordering.txt に記述されたテーブル名一覧を取得し、
        // バックアップテーブルとしてセットする
        List<String> backupTableList = Files.readAllLines(Paths.get(TESTDATA_BASE_DIR, "table-ordering.txt"));
        for (String backupTable :  backupTableList) {
            partialDataSet.addTable(backupTable);
        }

        ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
        replacementDatasetBackup.addReplacementObject("", NULL_STRING);
        this.backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
        try (FileOutputStream fos = new FileOutputStream(this.backupFile)) {
            FlatXmlDataSet.write(replacementDatasetBackup, fos);
        }
    }

    private void getBackupTableList() {
    }
    
    private void restoreDb(IDatabaseConnection conn)
            throws MalformedURLException, DatabaseUnitException, SQLException {
        if (this.backupFile != null) {
            IDataSet dataSet = new FlatXmlDataSetBuilder().build(this.backupFile);
            ReplacementDataSet replacementDatasetRestore = new ReplacementDataSet(dataSet);
            replacementDatasetRestore.addReplacementObject(NULL_STRING, null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDatasetRestore);
        }
    }

    private void loadTestData(Description description) {
        description.getAnnotations().stream()
                .filter(annotation -> annotation instanceof TestData)
                .forEach(annotation -> {
                    TestData testData = (TestData)annotation;
                    testDataLoader.load(testData.value());
                });
    }
    
}
  • private final List<String> BACKUP_TABLES = Arrays.asList( ... ) を削除します。
  • 定数文字列のフィールドに static を追加します。
  • backupDb メソッド内で table-order.txt から読み込んだテーブル名一覧を partialDataSet.addTable(...) に渡すよう変更します。

AssertOptions.java

package ksbysample.common.test;

public enum AssertOptions {

    EXCLUDE_COLUM
    , INCLUDE_COLUMN;
    
}

TableDataAssert.java

package ksbysample.common.test;

import org.dbunit.Assertion;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.filter.DefaultColumnFilter;

import javax.sql.DataSource;
import java.sql.SQLException;

public class TableDataAssert {

    private static final String NULL_STRING = "[null]";

    private final IDataSet dataSet;

    private final DataSource dataSource;

    public TableDataAssert(IDataSet dataSet, DataSource dataSource) {
        ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
        replacementDataset.addReplacementObject(NULL_STRING, null);
        this.dataSet = replacementDataset;
        this.dataSource = dataSource;
    }

    public void assertEquals(String tableName, String[] columnNames) throws DatabaseUnitException, SQLException {
        assertEquals(tableName, columnNames, AssertOptions.EXCLUDE_COLUM);
    }

    public void assertEquals(String tableName, String[] columnNames, AssertOptions options)
            throws DatabaseUnitException, SQLException {
        ITable expected = expectedTable(tableName, columnNames, options);
        ITable actual = actualTable(tableName, columnNames, options);
        Assertion.assertEquals(expected, actual);
    }

    public void assertEqualsByExcludingColumn(String tableName, String[] columnNames)
            throws DatabaseUnitException, SQLException {
        assertEquals(tableName, columnNames, AssertOptions.EXCLUDE_COLUM);
    }

    public void assertEqualsByIncludingColumn(String tableName, String[] columnNames)
            throws DatabaseUnitException, SQLException {
        assertEquals(tableName, columnNames, AssertOptions.INCLUDE_COLUMN);
    }
    
    private ITable expectedTable(String tableName, String[] columnNames, AssertOptions options) throws DataSetException {
        ITable table = dataSet.getTable(tableName);
        if (columnNames != null) {
            table = columnFilter(table, columnNames, options);
        }
        return table;
    }

    private ITable actualTable(String tableName, String[] columnNames, AssertOptions options)
            throws DatabaseUnitException, SQLException {
        IDatabaseConnection conn = new DatabaseConnection(this.dataSource.getConnection());
        ITable table = conn.createDataSet().getTable(tableName);
        if (columnNames != null) {
            table = columnFilter(table, columnNames, options);
        }
        return table;
    }

    private ITable columnFilter(ITable table, String[] columnNames, AssertOptions options)
            throws DataSetException {
        if (options == AssertOptions.EXCLUDE_COLUM) {
            table = DefaultColumnFilter.excludedColumnsTable(table, columnNames);
        }
        else {
            table = DefaultColumnFilter.includedColumnsTable(table, columnNames);
        }
        return table;
    }
    
}
  • 以下の3つのメソッドを追加します。
    • 指定されたカラムのみで比較するか除外して比較するかを指定できる public void assertEquals(String tableName, String[] columnNames, AssertOptions options)
    • 指定されたカラムを除外して比較する public void assertEqualsByExcludingColumn(String tableName, String[] columnNames)
    • 指定されたカラムのみで比較する public void assertEqualsByIncludingColumn(String tableName, String[] columnNames)
    • private ITable columnFilter(ITable table, String[] columnNames, AssertOptions options)
  • expectedTable, actualTable メソッドの第3引数に AssertOptions options を追加します。
  • 既存の public void assertEquals(String tableName, String[] columnNames) は従来と同じ指定されたカラムを除外して比較する処理になるようにします。

LendingapprovalControllerTest.java

        @Test
        @TestData("src/test/resources/ksbysample/webapp/lending/web/lendingapproval/testdata/001")
        public void 確定ボタンをクリックした場合_却下と却下理由() throws Exception {
            // when ( Spock Framework のブロックの区分けが分かりやすかったので、同じ部分にコメントで付けてみました )
            mvc.authTanakaTaro.perform(TestHelper.postForm("/lendingapproval/complete", this.lendingapprovalForm_005).with(csrf()))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("lendingapproval/lendingapproval"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(xpath("//*[@id=\"lendingapprovalForm\"]/div/div/table/tbody/tr[1]/td[6]/input[@type=\"text\"]").doesNotExist())
                    .andExpect(xpath("//*[@id=\"lendingapprovalForm\"]/div[2]/div/table/tbody/tr[1]/td[6]/span").string("購入済です"));

            // then ( Spock Framework のブロックの区分けが分かりやすかったので、同じ部分にコメントで付けてみました )
            // DB
            IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/web/lendingapproval/assertdata/002"));
            TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
            tableDataAssert.assertEquals("lending_book", new String[]{"approval_result", "approval_reason", "version"}
                    , AssertOptions.INCLUDE_COLUMN);
            // メール
            assertThat(mailServerResource.getMessagesCount()).isEqualTo(1);
            MimeMessage mimeMessage = mailServerResource.getFirstMessage();
            assertThat(mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO))
                    .extracting(Object::toString)
                    .containsOnly("tanaka.taro@sample.com");
            assertThat(mimeMessage.getContent())
                    .isEqualTo(com.google.common.io.Files.toString(
                            new File("src/test/resources/ksbysample/webapp/lending/web/lendingapproval/assertdata/002/message.txt")
                            , Charsets.UTF_8));
        }
  • 確定ボタンをクリックした場合_却下と却下理由() テストメソッド内で lending_book テーブルのデータをチェックする tableDataAssert.assertEquals の第3引数に AssertOptions.INCLUDE_COLUMN を追加します。

TestHelper.java

package ksbysample.common.test;

import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

public class TestHelper {

    /**
     * Formクラスをパラメータに持つMockHttpServletRequestBuilderクラスのインスタンスを生成する
     * 
     * @param urlTemplate
     * @param form
     * @return
     * @throws IllegalAccessException
     */
    public static MockHttpServletRequestBuilder postForm(String urlTemplate, Object form) throws IllegalAccessException {
        MockHttpServletRequestBuilder request = post(urlTemplate).contentType(MediaType.APPLICATION_FORM_URLENCODED);
        setParameterFromForm(request, form, null, null);
        return request;
    }

    /**
     * FormValidator クラスのテスト等で使用するための Errors オブジェクト
     * ( MapBindingResult クラスのインスタンス ) を生成する
     * 
     * @return
     */
    public static Errors createErrors() {
        return new MapBindingResult(new HashMap<String, String>(), "");
    }
    
    /**
     * EntityクラスとFormクラスの値が同じかチェックする
     * 
     * @param entity
     * @param form
     * @throws IllegalAccessException
     */
    public static void assertEntityByForm(Object entity, Object form) throws IllegalAccessException {
        for (Field entityField : entity.getClass().getDeclaredFields()) {
            entityField.setAccessible(true);
            try {
                Field formField = form.getClass().getDeclaredField(entityField.getName());
                formField.setAccessible(true);
                assertThat(entityField.get(entity), is(formField.get(form)));
            }
            catch (NoSuchFieldException ignored) {}
        }
    }

    /**
     * Form クラスのインスタンスのフィールド名と値を request にセットする
     *
     * @param request
     * @param form
     * @throws IllegalAccessException
     */
    private static void setParameterFromForm(MockHttpServletRequestBuilder request, Object form, String fieldName, Integer arrayIndex) throws IllegalAccessException {
        for (Field field : form.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            if (field.get(form) == null) {
                request = request.param(makeParameterName(fieldName, field.getName(), arrayIndex), "");
            }
            else if (field.get(form) instanceof ArrayList<?>) {
                Integer i = 0;
                for (Object obj : (List<?>)field.get(form)) {
                    if (isPrimitiveOrString(obj)) {
                        request = request.param(makeParameterName(fieldName, field.getName(), i), obj.toString());
                    }
                    else {
                        setParameterFromForm(request, obj, makeParameterName(fieldName, field.getName(), arrayIndex), i);
                    }
                    i++;
                }
            }
            else if (isPrimitiveOrString(field.get(form))) {
                request = request.param(makeParameterName(fieldName, field.getName(), arrayIndex), field.get(form).toString());
            }
            else {
                setParameterFromForm(request, field.get(form), makeParameterName(fieldName, field.getName(), arrayIndex), null);
            }
        }
    }

    /**
     * 指定されたオブジェクトがプリミティブ型かチェックする
     *
     * @param obj
     * @return
     */
    private static boolean isPrimitiveOrString(Object obj) {
        boolean result = false;
        if (obj instanceof Byte) result = true;
        else if (obj instanceof Short) result = true;
        else if (obj instanceof Integer) result = true;
        else if (obj instanceof Long) result = true;
        else if (obj instanceof Character) result = true;
        else if (obj instanceof Float) result = true;
        else if (obj instanceof Double) result = true;
        else if (obj instanceof Boolean) result = true;
        else if (obj instanceof String) result = true;
        return result;
    }

    /**
     * MockHttpServletRequestBuilderクラスのインスタンス ( request ) にセットするパラメータ名を生成する
     *
     * @param rootFieldName
     * @param fieldName
     * @param arrayIndex
     * @return
     */
    private static String makeParameterName(String rootFieldName, String fieldName, Integer arrayIndex) {
        StringBuilder sb = new StringBuilder();
        if (rootFieldName != null) {
            sb.append(rootFieldName);
            if (arrayIndex != null) {
                sb.append("[");
                sb.append(arrayIndex);
                sb.append("]");
            }
            sb.append(".");
        }
        sb.append(fieldName);
        return sb.toString();
    }
    
}

TestDataLoader.java

package ksbysample.common.test;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;

@Component
public class TestDataLoader {

    private static final String NULL_STRING = "[null]";
    
    @Autowired
    private DataSource dataSource;

    public void load(String csvDir) {
        IDatabaseConnection conn = null;
        try {
            conn = new DatabaseConnection(dataSource.getConnection());

            IDataSet dataSet = new CsvDataSet(new File(csvDir));
            ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
            replacementDataset.addReplacementObject(NULL_STRING, null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {}
        }
    }

}
  • catch (Exception e) { ... } 内の処理を e.printStackTrace()throw new RuntimeException(e); へ変更します。
  • private static final String NULL_STRING = "[null]"; を追加し、クラス内で文字列を直接書いていた部分を置換します。
  • catch (Exception e) { ... } の位置を変更します。
  • if (conn != null) conn.close();try { ... } catch (Exception ignored) {} で囲みます。

履歴

2016/01/23
初版発行。
2016/01/28
* TestDataResource のメソッド名を hasNouseTestDataResourceAnnotation → hasNoUseTestDataResourceAnnotation へ修正しました。