DBUnit のユーティリティクラス

実際使ってみると色々あったのでユーティリティクラスを作りました。 テストが終わったらテスト前のデータベースの状態に戻して欲しかったり、メモリを消費せずに大量データを扱いたかったり。 ロールバックして何事もなかったように元に戻すという方法もありますが、今回は関連するテーブルをダンプしておいて、テスト終了後にリストアするという方法でユーティリティ化しました。 テストする側からテスト対象のトランザクションを制御できないような作りになっていても対処でき、個人的には結構よかったので、よろしければ活用ください。

サンプルソース

取り急ぎ、GitHubにサンプルソース一式あげています。 https://github.com/catoocraft/DbUnitUtils-SpringBoot サンプルでテストを実行するには、PostgreSQLでテスト対象となるテーブルの作成が必要です。

ポイント

  • 普段は実運用を意識したレコード件数をデータベースに置いておくために、テストの時だけ退避および復旧できる。
  • BeforeClass や BeforeAll が使えなく Before や BeforeEach で毎回呼び出しても、退避と復旧はテストクラス単位で実施する。
DBUnitを使うテストの場合、データベースに対して初期データを投入してコミットまでしてしまうので、他のテストに影響しないように当該テーブルの退避と復旧が必要でした。 またサービスのテストで使おうとするとクラス単位の初期化・後始末のアノテーションが機能しないので、参照カウンタを導入してテスト単位に毎回呼び出しても最初に退避して最後に復旧する必要がありました。 (これはちゃんとした方法があるのかも。。。)

DbUnitUtilsクラス

ユーティリティクラスはこれひとつです。
package com.catoocraft.demo.dbunit;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import org.dbunit.database.CachedResultSetTableFactory;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.ForwardOnlyResultSetTableFactory;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.AbstractDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.excel.XlsDataSet;
import org.dbunit.dataset.stream.StreamingDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlProducer;
import org.dbunit.operation.DatabaseOperation;
import org.xml.sax.InputSource;

public class DbUnitUtils {
    
    private static int dumpCounter = 0;
    private static File dumpFile = null;
    private static IDatabaseConnection dbconn = null;
    
    public static void dumpTables(String... excelPaths) throws Exception {
        
        if (dumpCounter++ > 0) {
            return;
        }
        
        if (dbconn == null) {
            dbconn = getDatabaseConection();
        }
        
        // 大きいサイズのテーブルをダンプできるようにResultSetファクトリを変更
        DatabaseConfig config = dbconn.getConfig();
        config.setProperty(DatabaseConfig.PROPERTY_RESULTSET_TABLE_FACTORY, new ForwardOnlyResultSetTableFactory());
        
        // Excel で指定されたデータセット内のテーブルを退避対象とする
        Set<String> tableSet = new HashSet<>();
        for (String path : excelPaths) {
            IDataSet dataSet = getDataSet(path);
            for (String tableName : dataSet.getTableNames()) {
                tableSet.add(tableName);
            }
        }
        
        QueryDataSet queryDataSet = new QueryDataSet(dbconn);
        for (String tableName : tableSet) {
            queryDataSet.addTable(tableName);
        }
        
        // ダンプの出力先となるテンポラリファイルを作成
        dumpFile = File.createTempFile("dump", ".xml");
        try (OutputStream fs = new FileOutputStream(dumpFile)) {
            FlatXmlDataSet.write(wrap(queryDataSet), fs);
        }
        
        // 変更したファクトリをデフォルトのファクトリに戻す
        config.setProperty(DatabaseConfig.PROPERTY_RESULTSET_TABLE_FACTORY, new CachedResultSetTableFactory());
    }
    
    public static void restoreTables() throws Exception {
        
        if (--dumpCounter > 0) {
            return;
        }
        
        try (InputStream is = new FileInputStream(dumpFile.getPath())) {
            FlatXmlProducer producer = new FlatXmlProducer(new InputSource(is));
            DatabaseOperation.DELETE_ALL.execute(dbconn, unwrap(new StreamingDataSet(producer)));
        }
        try (InputStream is = new FileInputStream(dumpFile.getPath())) {
            FlatXmlProducer producer = new FlatXmlProducer(new InputSource(is));
            DatabaseOperation.INSERT.execute(dbconn, unwrap(new StreamingDataSet(producer)));
        }
        
        dumpFile.delete();
        dbconn.close();
        dbconn = null;
    }
    
    public static void setUpTables(String excelPath) throws Exception {
        // Excel 用データセット作成
        IDataSet dataSet = getDataSet(excelPath);
        
        // データの全削除&挿入
        DatabaseOperation.CLEAN_INSERT.execute(dbconn, dataSet);
    }
    
    public static boolean updateTable(String sql) throws Exception {
        Connection con = dbconn.getConnection();
        try (PreparedStatement ps = con.prepareStatement(sql)) {
            return ps.execute();
        }
    }
    
    public static IDataSet getDataSet(String excelPath) throws Exception {
        URL url = ClassLoader.getSystemResource(excelPath);
        if (url == null) {
            throw new FileNotFoundException(excelPath);
        }
        File excelFile = new File(url.getFile());
        return new XlsDataSet(excelFile);
    }
    
    public static ITable getActualTable(String tableName) throws Exception {
        return dbconn.createDataSet(new String[] {tableName}).getTable(tableName);
    }
    
    private static IDatabaseConnection getDatabaseConection() throws Exception {
        Properties conf = new Properties();
        conf.load(DbUnitUtils.class.getClassLoader().getResourceAsStream("DbUnitUtils.properties"));
        String url = conf.getProperty("jdbc.url");
        String user = conf.getProperty("jdbc.user");
        String password = conf.getProperty("jdbc.password");
        String schema = conf.getProperty("jdbc.schema");
        Connection connection = DriverManager.getConnection(url, user, password);
        connection.setAutoCommit(true);
        return new DatabaseConnection(connection, schema);
    }

    private static ReplacementDataSet wrap(AbstractDataSet orignal) {
        ReplacementDataSet replacement = new ReplacementDataSet(orignal);
        replacement.addReplacementObject(null, "<null>");
        return replacement;
    }
    
    private static ReplacementDataSet unwrap(AbstractDataSet orignal) {
        ReplacementDataSet replacement = new ReplacementDataSet(orignal);
        replacement.addReplacementObject("<null>", null);
        return replacement;
    }
    
}

使い方

データベースの接続先は DbUnitUtils.properties で指定します。
jdbc.url=jdbc:postgresql://localhost:5432/postgres
jdbc.username=ycatoo
jdbc.password=
jdbc.schema=public
各テストの前で dumpTables して、各テストの後に restoreTables します。 各テストの中では setUpTables で前提となるレコードを投入してください。
@SpringBootTest
class ItemServiceTest {

    String[] ignoreColumns = new String[] { "created", "updated" };

    @Autowired
    ItemService service;

    @Test
    void save() throws Exception {
        DbUnitUtils.setUpTables("ItemService/setup.xlsx");
        Item item = new Item();
        item.id = 1002;
        item.title = "Orange";
        item.price = 200;
        Item actual = service.save(item);
        assertThat(actual.title, is("Orange"));
        assertThat(actual.price, is(200));
        assertThat(actual.version, is(0));

        // 実行結果
        ITable actualTable = DbUnitUtils.getActualTable("items");

        // 期待値
        IDataSet expectedDataSet = DbUnitUtils.getDataSet("ItemService/expected_save.xlsx");
        ITable expectedTable = expectedDataSet.getTable("items");

        //比較
        Assertion.assertEqualsIgnoreCols(expectedTable, actualTable, ignoreColumns);
    }

    @BeforeEach
    public void setUp() throws Exception {
        DbUnitUtils.dumpTables("ItemDao/setup.xlsx");
    }

    @AfterEach
    public void tearDown() throws Exception {
        DbUnitUtils.restoreTables();
    }
}

コメントを残す

メールアドレスが公開されることはありません。