Java
unittest
DBUnit

【超初心者向け】DBUnit超入門

はじめに

今回は、プログラム開発では大活躍!単体テストで使用できるフレームワーク、「DBUnit」の使い方を紹介します。

DBUnitとは、Javaで使用するテスティングフレームワークの一つなのですが、筆者が勉強した感じでは、記事が古かったり、それだけで完全に事足りるwebが少ないように見受けられたので、筆者の備忘と、この記事をご覧いただいた方への共有も兼ねて、こちらにまとめておきます。

参考としてwebに資料を記載いただいている優しい方も、たくさんいらっしゃるんですけどねorz

1. DBUnitとは

先にも記載しましたが、DBUnitとは、Javaプログラムのテスティングフレームワークの一つです。
テスティングフレームワークと聞くと、XUnitを想像されるかと思います。
今回の対象はJavaで実装したプログラムですから、よく使用するのはJUnitですが、DBUnitもJUnitと一緒に使います。

JUnitでは、プログラム中に処理したメソッドの戻り値や、引き渡したパラメータに対してチェックを行うことができますが、DBUnitでは、DBに更新した値に対してチェックを行うことができます。
その他に、テスト開始前に、DBにテストデータを積み込んだりすることもできます。

その存在自体を知らない人も少なくないようなのですが、使いこなせれば、単体テスト(以下、UT)が格段に「楽に、正確に」なりますので、ご興味があれば是非この記事を最後までご覧ください。

2. テストプロジェクトの構築

まずは、DBUnitのテストに使用するテストプロジェクトを構築します。
テスティングフレームワークのテストというのも、少しおかしい気もしますが、その辺は気にしないで行きます。

筆者は以前、Mavenの勉強をして、今Mavenを推しているので、今回のテストプロジェクトは、MavenとEclipseを使用して構築します。

Mavenプロジェクトの構築は、以前筆者が記載した、こちらの記事を参考にしていただけると幸いです。
プロジェクト名と構成は任意に変更してください。
今回筆者は「dbunit-test」とし、以下のライブラリを依存関係として追加ました。

プロジェクトツリーと、pom.xmlの設定内容を記載しておきます。

■プロジェクトツリー
プロジェクトルート
├─src
│  ├─main
│  │  ├─java
│  │  │  └─test
│  │  │      └─main
│  │  │              TestMain.java
│  │  │
│  │  └─resources
│  │
│  └─test
│      ├─java
│      │  └─test
│      │      └─main
│      │              TestMainTest.java
│      │
│      └─resources
│              logback.xml
│
├─target
│  ├─classes
│  │  │  logback.xml
│  │  │
│  │  └─test
│  │      └─main
│  │              TestMain.class
│  │
│  └─test-classes
│      │  logback.xml
│      │
│      └─test
│          └─main
│                  TestMainTest.class
│
├─data
│      Before.xml     --- 事前準備データ
│      After.xml      --- 結果検証データ
└  pom.xml

■pom.xml記載内容
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dbunit.test</groupId>
    <artifactId>dbunit-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <!-- 文字コードとJavaのバージョンの設定 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <!-- ロガーの設定 -->
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <!--
                今回DBにはOracleを使用するので、JDBCドライバはOracleのものを使用します。
                OracleのJDBCドライバはMavenのセントラルリポジトリにはないので、ローカルリポジトリにインストールしてあるものを使用します。
                DBUnitと直接の関係はありませんが、ライブラリをローカルリポジトリにインストールする方法は、
                最後に参考として記載します。
            -->
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc7</artifactId>
            <version>11.2.0</version>
        </dependency>

        <!-- テストに使用するライブラリ -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.dbunit</groupId>
            <artifactId>dbunit</artifactId>
            <!-- 記事執筆時点での最新 -->
            <version>2.5.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

今回この記事ではMavenを使用していますが、JUnitとDBUnitさえビルドパスに追加しておけば、Mavenである必要はりません。
JUnitはEclipseのプラグインでも構いませんので、お好みに合わせてプロジェクト設定は変更してください。

3. テスト用テーブルの構築

テストプログラムで実装するテスト用テーブルを構築します。
今回は、Oracleインストール時に自動で生成してくれるDBと伝統のSCOTTユーザを使用します。
使用するテーブルも、教科書などでおなじみ、EMPテーブルです。
Oracleインストール時にデフォルトをで構築している場合は、初めから自動で作成されていますが、テーブルのDDLを記載しておきます。

■EMPテーブルのDDL
CREATE TABLE SCOTT.EMP ( 
    ID                          NUMBER          NOT NULL,
    NAME                        VARCHAR2(255)   NOT NULL,
    AGE                         NUMBER          NOT NULL,
    SALARY                      NUMBER,
    JOB_TYPE                    VARCHAR2(20),
    HIREDATE                    TIMESTAMP(6),
    DEPARTMENT_ID               NUMBER
);

テーブルは構築しますが、今回はDBUnitとしてテストデータを積み込みたいので、テーブルは空のままにしておいてください。

4. テストプログラムの実装

プロジェクトのスケルトンを構築したところで、テストプログラムを実装します。
あくまでもサンプル程度なので、1テーブルのレコードをUPDATEするだけのプログラムです。

4.1. テスト対象プログラムの実装

テスト対象とするサンプルプログラムは、「TestMain.java」です。
テスト用プログラムでは、logbackを使用していますが、こちらはおまけなので、使用する必要はありません。
logbackを使用すれば、DBUnitのデバッグログが見られるので、結構便利です。

■TestMain.java
package test.main;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * DBUnitテストのmainを持つクラス。
 * @author tarosa0001
 */
public class TestMain {
    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(TestMain.class);

    /** 実行するSQL */
    private static final String SQL
            = " update "
            + "         EMP "
            + "     set "
            + "     name = 'tiger' "
            + "     where "
            + "         id = 1";

    /**
     * @param args
     */
    public static void main(String[] args) {
        logger.info("処理開始");

        // ---------------------------------
        // DBを更新する
        // ---------------------------------

        try(Connection conn = DriverManager.getConnection(
                "jdbc:oracle:thin:@localhost:1521:ORCL", "scott", "tiger");
                PreparedStatement stmt = conn.prepareStatement(SQL);
                ) {
            conn.setAutoCommit(false);
            int i = stmt.executeUpdate();

            // 処理件数を表示する
            logger.info("処理件数:[" + i + "]");

            conn.commit();
        } catch(Exception e) {
            logger.error("エラー", e);
        }

        logger.info("処理終了");
    }
}

4.2. JUnit + DBUnitテストソースの実装

ここからが本番です。
JUnitとDBUnitを使用したテストソースを実装します。
実装対象は「TestMainTest.class」です。
テストプログラムの内容を記載します。
ここでは前処理でテストデータの積み込みを、テストメソッドで結果データの検証をしていますが、それぞれのデータの内容は後述します。

■TestMainTest.class
package test.main;

import static org.junit.Assert.*;

import java.io.File;

import org.dbunit.Assertion;
import org.dbunit.IDatabaseTester;
import org.dbunit.JdbcDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.filter.DefaultColumnFilter;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * [テストクラス]<br>
 * JUnitとDBUnitを使用してテストを行う。<br>
 * <br>
 * @author tarosa0001
 */
public class TestMainTest {
    /** ロガー */
    private Logger logger = LoggerFactory.getLogger(TestMain.class);

    /** DBUnitのテスター */
    private static IDatabaseTester databaseTester;

    /**
     * [前処理]<br>
     * DBに事前データを準備する。<br>
     * <br>
     * @throws java.lang.Exception
     */
    @Before
    public void setUp() throws Exception {
        logger.info("前処理開始");

        // --------------------------------------
        // 事前準備データのINSERT
        // --------------------------------------
        // 事前準備データのINSERTにはスキーマも合わせて指定する
        databaseTester = new JdbcDatabaseTester("oracle.jdbc.driver.OracleDriver",
                "jdbc:oracle:thin:@localhost:1521:ORCL", "scott", "tiger", "scott");

        // --------------------------------------
        // テストデータ投入
        // --------------------------------------
        IDataSet dataSet = new FlatXmlDataSetBuilder().build(new File("./data/Before.xml"));
        databaseTester.setDataSet(dataSet);
        // DELETE→INSERTで事前準備データを用意する
        databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
                databaseTester.onSetup();

        logger.info("前処理終了");
    }

    /**
     * [後処理]<br>
     * テスト後の後処理を行う。<br>
     * DBUnitの後片付けを行う。<br>
     * <br>
     * @throws java.lang.Exception
     */
    @After
    public void tearDown() throws Exception {
        databaseTester.setTearDownOperation(DatabaseOperation.NONE);
        databaseTester.onTearDown();
    }

    /**
     * [テスト]<br>
     * DBUnitを使用して、DBの更新結果を検証する。<br>
     */
    @Test
    public void test() {
        logger.info("JUnit + DBUnitによるテスト開始。");

        TestMain.main(null);

        try {
            // ----------------------------------
            // DBUnitで更新後データチェック
            // ----------------------------------
            IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(new File("./data/After.xml"));
            ITable expectedTable = expectedDataSet.getTable("EMP");

            IDataSet databaseDataSet = databaseTester.getConnection().createDataSet();
            ITable actualTable = databaseDataSet.getTable("EMP");

            // 時間に対するAssertionはほぼ確実に失敗するので検証対象から除外する
            ITable filteredExpectedTable = DefaultColumnFilter.excludedColumnsTable(
                    expectedTable, new String[]{"HIREDATE"});
            ITable filteredActualTable;
            filteredActualTable = DefaultColumnFilter.excludedColumnsTable(
                    actualTable, new String[]{"HIREDATE"});

            // ---------------------------------------------------------------
            // 更新結果の検証はJUnitではなくDBUnitのAssertionを使用する
            // ---------------------------------------------------------------
            Assertion.assertEquals(filteredExpectedTable, filteredActualTable);
        } catch (Exception e) {
            logger.error("エラー", e);
            fail("予期しないエラーでテストが失敗しました。");
        }

        logger.info("JUnit + DBUnitによるテスト開始。");
    }
}

4.3. 準備データの積み込み
DBUnitを使用して事前準備データを積み込みます。
事前準備データも、結果検証用データも、どちらの定義もXMLを使用します。

筆者はXMLが大嫌いなので、このような類のものは基本的にはExcelとかCSVとかで作りたいところなのですが、CSVでは1ファイル1テーブルしか定義できない、Excelではシート名がテーブル名に対して長さが足りないなど、どうしようもなく不便な要素があるので、DBUnitのテストデータはXMLを使用します。

事前準備データと、結果検証用データのXMLの内容を記載します。
事前準備データは「Before.xml」、結果検証用データは「After.xml」です。

■Before.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <!--
        データセット用のXMLでは数値データも""で囲む
        DATEやTIMESTAMP型の日付は「-」つなぎで指定する
    -->
    <EMP
            ID="1"
            NAME="scott"
            AGE="22"
            SALARY="200000"
            JOB_TYPE="employee"
            HIREDATE="2017-01-01 12:34:56"
            DEPARTMENT_ID="1"
    />
</dataset>

■After.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <!--
        日付は検証対象外だが一応記載します。
        フィルターできていることを検証するために、わざと違う日付を入力しておきます。
    -->
    <EMP
            ID="1"
            NAME="tiger"
            AGE="22"
            SALARY="200000"
            JOB_TYPE="employee"
            HIREDATE="2017-12-31 24:12:36"
            DEPARTMENT_ID="1"
    />
</dataset>

上記ではDATE型の日時は固定にしていますが、SYSDATEのように、現在日時をセットすることも可能です。
その場合は、こちらが参考になりますのでご覧下さい。

5. テストプログラムの実行

さて、ここまでで準備作業を終えましたので、テスト用のプログラムを動かしてみようと思います。
ただし、DBの変化を見たいので、実行前の状態は記録しておきましょう。

5.1. 実行前状態

実行前のテーブルは、レコードが空の状態です。
レコードが入っていても構わないのですが、いったんわかりやすくからの状態からスタートします。

■実行前テーブル状態
picture1.JPG

5.2. テストの実行

では、テストを実行します。
構築したプロジェクトを、JUnitテストとして実行してください。
結果は次のように、エラーはなく正常に終了したことと思います。

■JUnit実行結果
picture2.JPG

これでJUnit + DBUnitによるテストが構築→実行できました。
今回は事前準備データも結果データも一つしか用意していませんが、複数のデータを用意し、テストごとに読み込むXMLを変えれば、テストケースごとに新しいデータを自動的に積み込んで、期待した結果を自動で検証することができます。

6. テストの分解

ここまで、JUnit + DBUnitで自動テストを行う方法を紹介してきましたが、いかがでしょうか?
必ずしも自動テストがよいかと問われれば、そういうわけではありませんが、何度もテストをやり直したり、何度もテスト結果を取り直したりするのって、結構面倒なので、テストが自動化できるのは、多くの場合では、大きなメリットとなることと思います。

ここまでは、駆け足でDBUnitを実行することだけを説明してきましたが、ここからは、実行したテストをステップごとに分解していきます。
具体的には、次のステップに分解し、それぞれのステップで、プログラムの動きと、DBの状態を見ていきます。

  1. 事前準備データの積み込み
  2. テスト対象プログラム中の状態
  3. テスト結果の検証

では、Eclipse上でデバッグ実行しつつ、テストプログラムの動きを見ていきましょう。

6.1. 事前準備データの積み込み

まずは、DBUnitによる事前準備データの積み込み時の動きを確認しましょう。

6.1.1. 実行前状態

テーブルは、いったん空の状態戻してあります。

■実行前テーブル状態
picture1.JPG

6.1.2. 前処理終了直後

前処理終了直後には、Before.xmlから読み込んだデータがきっちり入っていることがお分かりいただけるかと思います。
テスト対象プログラムは処理中にDBをUPDATEしているので、レコードが入っていないとうまく動作しない母図なのですが、この前処理でレコードが挿入されていず状態で開始するので正常に動作するわけです。

■プログラムの実行ステップ
picture4.jpg

■DBの状態
picture3.jpg

6.1.3. テストプログラム中の状態

テストプログラム実行中の状態を見てみましょう。
前処理で更新対象のレコードが挿入されているので、UPDATEは正常に実行を終了し、DBの状態が変わっていることが見て取れるかと思います。

■UPDATE実行前
picture5.jpg

■UPDATE実行後
a4886ebe-6a13-abf8-1d56-8a5b4f694f3f.jpeg

■実行後のDBの状態
picture7.jpg

6.1.4. テスト結果の検証

最後にテスト結果の検証を見てみましょう。
DBの状態が期待通りになっているので、DBUnitのアサーションにより、テストを失敗することはなく、アサーションの処理を正常に実行しています。
アサーション実行後の最終結果は、先に記載した通りです。

■アサーション実行前
picture8.jpg

■アサーション実行後
picture9.jpg

7. 最後に

長くなった今回の記事も、これで最後です。
ここまで長くにわたり、DBunitについて紹介してきましたが、いかがでしょうか?
DBUnitは、単体テストを自動化するためのツールですが、うまく使えば単体テスト以外でも、テストデータの積み込みができたり、いろいろと便利なツールになります。

ただ、便利な反面、結構手間もかかるので、いつでも使えるものではありません。
今回の記事ではテストデータが一つしかなかったので、それほど手間ではありませんでしたが、準備しなければならないテーブルがたくさんあったり、一気にたくさんのレコードを処理するようなプログラムだと、検証を自動化しようにも、膨大な量のテストデータを準備しなければならないので、相当な手間です。

そのことも踏まえたうえで、うまく使っていただければ、筆者としてそれほどにうれしいことはありません。

8. 参考

今回の記事では直接記載しなかった、参考を記載します。
記事執筆にあたり、次のWebを参考にしました。
記事を記載しくださった管理人、作者の皆様、ありがとうございます。

■Mavenのローカルリポジトリにライブラリをインストールする方法
http://sinsengumi.net/blog/2012/12/maven%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%A7%E6%8F%90%E4%BE%9B%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E3%82%B5%E3%83%BC%E3%83%89%E3%83%91%E3%83%BC%E3%83%86%E3%82%A3jar/

■DBUnitで
http://jyukutyo.hatenablog.com/entry/20060630/1151636345