JUnit

JUnit4でDBUnitの@DatabaseSetupや@ExpectedDatabaseというアノテーションを利用してみた

DBUnitというツールを利用すると、データベースのデータ検索・追加・更新・削除のテストを実施できるが、その際、テスト前にテストデータを設定する@DatabaseSetupアノテーションや、テスト後のデータを検証する@ExpectedDatabaseアノテーションを利用すると便利である。

今回は、JUnit4でDBUnitの@DatabaseSetupや@ExpectedDatabaseというアノテーションを利用したテストを行うサンプルプログラムを作成してみたので、共有する。

前提条件

下記記事の実装が完了していること。

Oracle上でSpring Bootの@Transactionalアノテーションの挙動を調べてみたSpring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でD...

作成したサンプルプログラムの内容

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

build.gradleの内容は以下の通りで、DBUnitやMyBatisのテストを行うためのライブラリを追加している。

plugins {
    id 'org.springframework.boot' version '2.1.7.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    compileOnly 'org.projectlombok:lombok:1.18.10'
    annotationProcessor 'org.projectlombok:lombok:1.18.10'
    compile files('lib/ojdbc6.jar')
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.1'
    // DBUnitについての設定を追加
    testCompile group: 'org.dbunit', name: 'dbunit', version: '2.6.0'
    testCompile group: 'com.github.springtestdbunit', name: 'spring-test-dbunit', version: '1.3.0'
    // MyBatisのテストを行うための設定を追加
    testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.1.3'
}

ユーザーデータエンティティのクラスは以下の通りで、@NoArgsConstructorや@AllArgsConstructorアノテーションを付与している。

package com.example.demo.mapper;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * ユーザーデータテーブル(user_data)アクセス用エンティティ
 */
@Data
// @NoArgsConstructorアノテーションで引数のない
// コンストラクタを、@AllArgsConstructorアノテーションで
// 全てのフィールドを引数に持つコンストラクタを定義している
@NoArgsConstructor
@AllArgsConstructor
public class UserData {

    /** ID */
    private long id;

    /** 名前 */
    private String name;

    /** 生年月日_年 */
    private int birthY;

    /** 生年月日_月 */
    private int birthM;

    /** 生年月日_日 */
    private int birthD;

    /** 性別 */
    private String sex;

    /** メモ */
    private String memo;

    /** 性別(文字列) */
    private String sex_value;

}

ユーザーデータテーブルにアクセスするMapperは以下の通りで、findAllメソッド・createメソッドを追加している。

package com.example.demo.mapper;

import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface UserDataMapper {

    /**
     * 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する
     * @param id ID
     * @return ユーザーデータテーブル(user_data)の指定したIDのデータ
     */
    UserData findById(Long id);

    /**
     * 指定したユーザーデータテーブル(user_data)のデータを更新する
     * @param userData ユーザーデータテーブル(user_data)の更新データ
     */
    void update(UserData userData);

    /**
     * ユーザーデータテーブル(user_data)の全データを取得する
     * @return ユーザーデータテーブル(user_data)の全データ
     */
    List<UserData> findAll();

    /**
     * 指定したユーザーデータテーブル(user_data)のデータを追加する
     * @param userData ユーザーデータテーブル(user_data)の追加データ
     */
    void create(UserData userData);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserDataMapper">
    <resultMap id="userDataResultMap" type="com.example.demo.mapper.UserData" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="birthY" property="birthY" jdbcType="VARCHAR" />
        <result column="birthM" property="birthM" jdbcType="VARCHAR" />
        <result column="birthD" property="birthD" jdbcType="VARCHAR" />
        <result column="sex" property="sex" jdbcType="VARCHAR" />
        <result column="memo" property="memo" jdbcType="VARCHAR" />
        <result column="sex_value" property="sex_value" jdbcType="VARCHAR" />
    </resultMap>
    <select id="findById" parameterType="java.lang.Long" resultMap="userDataResultMap">
        SELECT
            id
          , name
          , birth_year as birthY
          , birth_month as birthM
          , birth_day as birthD
          , sex
          , memo
          , CASE sex
            WHEN '1' THEN '男'
            WHEN '2' THEN '女'
            ELSE ''
            END AS sex_value
        FROM USER_DATA
        WHERE id = #{id}
    </select>
    <update id="update" parameterType="com.example.demo.mapper.UserData">
        UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY}
          , birth_month = #{birthM}, birth_day = #{birthD}
          , sex = #{sex}, memo = #{memo,jdbcType=VARCHAR}
        WHERE id = #{id}
    </update>
    <select id="findAll" resultMap="userDataResultMap">
        SELECT
            id
          , name
          , birth_year as birthY
          , birth_month as birthM
          , birth_day as birthD
          , sex
          , memo
          , CASE sex
            WHEN '1' THEN '男'
            WHEN '2' THEN '女'
            ELSE ''
            END AS sex_value
        FROM USER_DATA
    </select>
    <insert id="create" parameterType="com.example.demo.mapper.UserData">
        INSERT INTO USER_DATA (
              id
            , name
            , birth_year
            , birth_month
            , birth_day
            , sex
            , memo
        ) VALUES (
              #{id}
            , #{name}
            , #{birthY}
            , #{birthM}
            , #{birthD}
            , #{sex}
            , #{memo,jdbcType=VARCHAR}
        )
    </insert>
</mapper>

UserDataTest1クラスの内容は以下の通りで、@DatabaseSetupアノテーションの検証を行っている。

package com.example.demo.mapper;

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.TransactionDbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.junit.Before;
import org.junit.Test;
import org.junit.After;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;

// JUnit4ベースでMyBatisのテストを実行する
@RunWith(SpringRunner.class)
@MybatisTest
// デフォルトのDBでなく、実際のDBを利用する
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
// テストを実行する際のリスナーを定義する
// DependencyInjectionTestExecutionListenerとDirtiesContextTestExecutionListenerで、
// SpringのDI機能を利用できるようにし、TransactionDbUnitTestExecutionListenerと
// DbUnitTestExecutionListenerで、トランザクション管理やDBデータの設定・検証・後処理が
// できるようにしている
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class
        , DirtiesContextTestExecutionListener.class
        , TransactionDbUnitTestExecutionListener.class
        , DbUnitTestExecutionListener.class})
// ExcelのデータをDBに設定する際、DemoXlsDataSetLoaderクラスを利用し、
// DB接続する際、(DemoTestDbConfigクラスの)dbUnitDatabaseConnectionという
// データソースコネクションファクトリを利用する
@DbUnitConfiguration(dataSetLoader = DemoXlsDataSetLoader.class
        , databaseConnection = {"dbUnitDatabaseConnection"})
public class UserDataTest1 {

    /**
     * ユーザーデータテーブル(user_data)へアクセスするマッパー
     */
    @Autowired
    private UserDataMapper userDataMapper;

    /**
     * 各テストメソッドを実行する前に行う処理を定義する.
     */
    @Before
    public void beforeTest() {
        System.out.println();
        System.out.println("*** UserDataTest1クラス テスト結果 start ***");
    }

    /**
     * テストを実行する前に、databaseSetupTest.xlsxに定義したデータを読み込み、
     * 取得したデータを検証する.
     */
    @Test
    @DatabaseSetup("/com/example/demo/xls/databaseSetupTest.xlsx")
    public void userDataTest() {
        // @DatabaseSetupアノテーションで設定されたデータを確認
        System.out.println("*** ユーザーデータテーブル(user_data)のデータ ***");
        List<UserData> userDataList = userDataMapper.findAll();
        for (UserData userData : userDataList) {
            System.out.println(userData);
        }
        assertEquals(expectedUserDataList(), userDataList);
    }

    /**
     * 予想されるテスト結果のデータを定義する.
     * @return 予想されるテスト結果のデータ
     */
    private List<UserData> expectedUserDataList() {
        List<UserData> userDataList = new ArrayList<>();
        UserData userData1 = new UserData(1, "テスト プリン1"
                , 2012, 2, 10, "1", null, "男");
        UserData userData2 = new UserData(2, "テスト プリン2"
                , 2013, 3, 15, "2", null, "女");
        UserData userData3 = new UserData(3, "テスト プリン3"
                , 2015, 4, 21, "2", "テスト3", "女");
        userDataList.add(userData1);
        userDataList.add(userData2);
        userDataList.add(userData3);
        return userDataList;
    }

    /**
     * 各テストメソッドを実行した後に行う処理を定義する.
     */
    @After
    public void afterTestClass() {
        System.out.println("*** UserDataTest1クラス テスト結果 end ***");
        System.out.println();
    }
}

UserDataTest2クラスの内容は以下の通りで、@DatabaseSetupと@ExpectedDatabaseのアノテーションの検証を行っている。

package com.example.demo.mapper;

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.TransactionDbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import com.github.springtestdbunit.assertion.DatabaseAssertionMode;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;

import java.util.List;

@RunWith(SpringRunner.class)
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class
        , DirtiesContextTestExecutionListener.class
        , TransactionDbUnitTestExecutionListener.class
        , DbUnitTestExecutionListener.class})
@DbUnitConfiguration(dataSetLoader = DemoXlsDataSetLoader.class
        , databaseConnection = {"dbUnitDatabaseConnection"})
public class UserDataTest2 {

    /**
     * ユーザーデータテーブル(user_data)へアクセスするマッパー
     */
    @Autowired
    private UserDataMapper userDataMapper;

    /**
     * 各テストメソッドを実行する前に行う処理を定義する.
     */
    @Before
    public void beforeTest() {
        System.out.println();
        System.out.println("*** UserDataTest2クラス テスト結果 start ***");
    }

    /**
     * テストを実行した後で、expectedDatabaseTest.xlsxに定義したデータが
     * 追加されることを検証する.
     */
    // @ExpectedDatabaseアノテーションで、assertionMode属性に
    // 「DatabaseAssertionMode.NON_STRICT_UNORDERED」を指定することで、
    // expectedDatabaseTest.xlsxに記載のあるテーブル・カラムのみを、行の順序を無視して
    // 検証できるようにしている
    @Test
    @DatabaseSetup("/com/example/demo/xls/databaseSetupTest.xlsx")
    @ExpectedDatabase(value="/com/example/demo/xls/expectedDatabaseTest.xlsx"
            , assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)
    public void userDataTest() {
        UserData userDataAdd = new UserData(4, "テスト プリン4"
                , 2016, 5, 6, "1", "テスト4", "男");
        userDataMapper.create(userDataAdd);

        System.out.println("*** ユーザーデータテーブル(user_data)のデータ ***");
        List<UserData> userDataList = userDataMapper.findAll();
        for (UserData userData : userDataList) {
            System.out.println(userData);
        }
    }

    /**
     * 各テストメソッドを実行した後に行う処理を定義する.
     */
    @After
    public void afterTestClass() {
        System.out.println("*** UserDataTest2クラス テスト結果 end ***");
        System.out.println();
    }
}

UserDataTest1クラス・UserDataTest2クラスの@DatabaseSetupアノテーションで読み込んでいるエクセルデータの内容は、以下の通り。
databaseSetupTest_excel

UserDataTest1クラス・UserDataTest2クラスの@ExpectedDatabaseアノテーションで検証しているエクセルデータの内容は、以下の通り。
expectedDatabaseTest_excel

UserDataTest1クラス・UserDataTest2クラスで呼ばれる、DB接続定義を行うクラスの内容は、以下の通り。

package com.example.demo.mapper;

import com.github.springtestdbunit.bean.DatabaseConfigBean;
import com.github.springtestdbunit.bean.DatabaseDataSourceConnectionFactoryBean;
import org.dbunit.ext.oracle.Oracle10DataTypeFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

// JUnit4ベースでMyBatisのテストを実行する際のDB接続定義

// @MybatisTestアノテーションでテストを実行する際、
// Spring Bootアプリケーションが起動するようにする。

// その際、com.example.demo.mapperフォルダ下のインスタンス
// (UserDataMapper)のみがDIで使用できるようになる。
@SpringBootApplication
@PropertySource(value = {"classpath:test.properties"})
public class DemoTestDbConfig {

    /**
     * Oracleのデータソースプロパティ(UT)を生成する.
     * @return Oracleのデータソースプロパティ(UT)
     */
    @Bean(name = {"datasourceOraProperties"})
    @Primary
    @ConfigurationProperties(prefix = "spring.datasourceut")
    public DataSourceProperties datasourceOraProperties() {
        return new DataSourceProperties();
    }

    /**
     * Oracleのデータソース(UT)を生成する.
     * @param properties Oracleのデータソースプロパティ(UT)
     * @return Oracleのデータソース(UT)
     */
    @Bean(name = {"dataSourceOra"})
    @Primary
    public DataSource datasourceOra(
            @Qualifier("datasourceOraProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }

    /**
     * Oracleのトランザクションマネージャ(UT)を生成する.
     * @param dataSourceOra Oracleのデータソース(UT)
     * @return Oracleのトランザクションマネージャ(UT)
     */
    @Bean(name = {"txManagerOra"})
    @Primary
    public PlatformTransactionManager txManagerOra(
            @Qualifier("dataSourceOra") DataSource dataSourceOra) {
        return new DataSourceTransactionManager(dataSourceOra);
    }

    /**
     * DB接続設定(UT)を生成する.
     * @return DB接続設定(UT)
     */
    @Bean(name = {"dbUnitDatabaseConfig"})
    public DatabaseConfigBean dbUnitDatabaseConfig() {
        DatabaseConfigBean bean = new DatabaseConfigBean();
        bean.setAllowEmptyFields(true);
        bean.setDatatypeFactory(new Oracle10DataTypeFactory());
        return bean;
    }

    /**
     * データソースコネクションファクトリ(UT)を生成する.
     * @param dbUnitDatabaseConfig DB接続設定(UT)
     * @param dataSourceOra         Oracleのデータソース(UT)
     * @return データソースコネクションファクトリ(UT)
     */
    @Bean(name = {"dbUnitDatabaseConnection"})
    public DatabaseDataSourceConnectionFactoryBean dbUnitDatabaseConnection(
            @Qualifier("dbUnitDatabaseConfig") DatabaseConfigBean dbUnitDatabaseConfig
          , @Qualifier("dataSourceOra") DataSource dataSourceOra) {
        DatabaseDataSourceConnectionFactoryBean bean 
            = new DatabaseDataSourceConnectionFactoryBean(dataSourceOra);
        bean.setDatabaseConfig(dbUnitDatabaseConfig);
        bean.setSchema("USER01");
        return bean;
    }
}

また、上記クラスで読み込むDB接続は、以下のプロパティファイルから読み込んでいる。

# DB接続先
spring.datasourceut.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasourceut.username=USER01
spring.datasourceut.password=USER01
spring.datasourceut.driverClassName=oracle.jdbc.driver.OracleDriver

UserDataTest1クラス・UserDataTest2クラスで呼ばれる、エクセルデータをDBに設定する定義を行うクラスの内容は、以下の通り。

package com.example.demo.mapper;

import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.excel.XlsDataSet;
import org.springframework.core.io.Resource;

import java.io.IOException;
import java.io.InputStream;

public class DemoXlsDataSetLoader extends AbstractDataSetLoader {

    /**
     * ExcelのデータをDBに格納する際の変換仕様を定義する.
     * @param resource リソースオブジェクト
     * @return 変換後データセット
     * @throws IOException      入出力例外
     * @throws DataSetException データセット例外
     */
    @Override
    protected IDataSet createDataSet(Resource resource) throws IOException
          , DataSetException {
        try (InputStream inputStream = resource.getInputStream()) {
            XlsDataSet xlsDataSet = new XlsDataSet(inputStream);
            ReplacementDataSet replacementDataSet = new ReplacementDataSet(xlsDataSet);
            // Excel上で「(NULL)」という記載は、DB上ではNULL値に変換する
            replacementDataSet.addReplacementObject("(NULL)", null);
            return replacementDataSet;
        }
    }

}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/junit4-dbunit-annotation/demo



【PR】「Filmora」は初心者でも本格的な動画編集ができる大変便利なツールだった「Filmora」は初心者でも使いやすい動画編集ツールで、テンプレートとして利用できるテキスト・動画・音楽などが充実していると共に、複数...

サンプルプログラムの実行結果

サンプルプログラムの実行結果は、以下の通り。

1) テスト実行前のデータは、以下の通り。
サンプルプログラムの実行結果_1

2) UserDataTest1クラスを実行した時のログ出力内容は、以下の通りで、databaseSetupTest.xlsxのデータがDBに設定されたことが確認できる。
サンプルプログラムの実行結果_2

3) UserDataTest2クラスを実行した時のログ出力内容は、以下の通りで、テスト実行後にexpectedDatabaseTest.xlsxのデータがDBに設定されていることが確認できる。
サンプルプログラムの実行結果_3

4) テスト実行後のデータは、以下の通りで、実際のDBデータは更新されずロールバックされることが確認できる。
サンプルプログラムの実行結果_4

要点まとめ

  • DBUnitというツールを利用してDB回りのテストを行う際、テスト前にテストデータを設定する@DatabaseSetupアノテーションや、テスト後のデータを検証する@ExpectedDatabaseアノテーションを利用すると便利である。