こんにちは。
キャスレーコンサルティングLS(リーディング・サービス)部の藁科です。

「JUnitのアサーションライブラリAssertJ」をテーマに、三回に分けてご紹介しております。

・第一回:「JUnitのアサーションライブラリAssertJ ~環境構築編~」
・第二回:「JUnitのアサーションライブラリAssertJ ~AssertJの利用メリットとは~」

今回は第三回目、「DbSetupとAssertJ-DBを利用したデータベースのテスト」についてです。

今回は、Doma2を利用してDBにアクセスするプログラムに対し
DbSetupとAssertJ-DBを利用してテストする方法について、ご紹介いたします。

DbSetupとAssertJ-DBの紹介

Java単体で書かれたコードでは、第1回目、第2回目で紹介した方法で単体テストを行うことは可能です。
しかし、DBにアクセスするプログラムを確認したいケースも発生すると思います。

DBのテストを行いたい場合、DBUnitが有名ですが、
事前にDBで利用したいデータをXMLやExcelで用意する必要があり、少し手間です。

Javaだけでテストコードを書きたい!そんなときにお勧めしたいのがDbSetupです。

DbSetupは、JavaでDBで利用したいデータを定義しておき、
実際にテストを行うときに投入することが出来ます。

続いて、DBのデータに対して変更(登録、更新、削除)するプログラムがあるとします。

実行後、正しく変更できたか確認したい場合、
変更対象のデータを取得して確認する方法を取っていることがあると思います。

このようなDBのテストを行いたい場合、AssertJ-DBを利用すると便利です。

AssertJ-DBはDBの差分を自動で抽出してくれるので、
変更対象のデータを取得せずにアサーションを書くことができます。

実際にサンプルコードを書いて、DBのテストを実施してみましょう。

環境構築 ~サンプルコード編~

今回は、Doma2というDBのO/Rマッパーを利用して、PostgreSQLにアクセスするプログラムを
サンプルコードとしてテストしたいと思います。

※Doma2の詳細は「https://doma.readthedocs.io/ja/stable/」をご参照ください。

※PostgreSQLの詳細は「https://www.postgresql.org/」をご参照ください。

環境構築は第一回目の「JUnitのアサーションライブラリAssertJ ~環境構築編~」で紹介した
環境をベースに必要な設定を追加していきます。

1.PostgeSQLのインストール

下記よりインストーラーをダウンロードして、
設定はデフォルトのままでインストールしてください。

https://www.enterprisedb.com/software-downloads-postgres/

※本記事では、パスワードは「postgres」としています。

2.Mavenの追記

pom.xmlに、以下の追記をします。

2.1.依存関係の追加

Doma2とPostgeSQLを利用する為に、依存関係の設定をします。

タグ内に、以下の追記をします。

  <dependency>
    <groupId>org.seasar.doma</groupId>
    <artifactId>doma</artifactId>
    <version>2.19.2</version>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.2</version>
  </dependency>

 

 

 

2.2.ビルドの設定

続いて、Doma2は注釈処理を使用して
コンパイル時にDaoからDaoImplを、自動生成することが出来ます。

以下の設定を追加します。

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.7.0</version>
          <configuration>
            <source>1.8</source>
            <target>1.8</target>
            <encoding>UTF-8</encoding>
            <compilerArgument>-proc:none</compilerArgument>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.bsc.maven</groupId>
        <artifactId>maven-processor-plugin</artifactId>
        <version>3.3.3</version>
        <executions>
          <execution>
            <id>process</id>
            <goals>
              <goal>process</goal>
            </goals>
            <phase>process-resources</phase>
            <configuration>
              <outputDirectory>${project.build.directory}/apt_generated</outputDirectory>
              <failOnError>false</failOnError>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

 

 

 

以上でMavenの設定は完了です。
Mavenを更新して、必要なjarをローカルリポジトリにダウンロードします。

3.Eclipseを使ったビルドの設定

3.1.注釈処理を有効化する

exampleを右クリックして、プロパティを開きます。
左のメニューからJava コンパイラー → 注釈処理を選択します。
以下の項目を全てチェックします。
✓プロジェクト固有の設定を可能にする
✓注釈処理を使用可能にする
✓エディターでの処理を使用可能にする

3.2.ファクトリー・パスの設定

上記プロパティ画面の左のメニューから、
Java コンパイラー → 注釈処理 → ファクトリー・パスを選択します。

下記項目をチェックします。
✓プロジェクト固有の設定を可能にする
Jarの追加ボタンを押下して、ビルドパスで指定しているDomaのjarを追加します。
以上でDoma2において、PostgreSQLにアクセスするための環境構築が完了です。

DBにアクセスするコードの作成

1.DDL

1.1.テーブル

CREATE TABLE employee
(
  id integer NOT NULL,
  name character varying(255) NOT NULL,
  age integer,
  hire_date date NOT NULL,
  enrollment boolean NOT NULL,
  version integer NOT NULL,
  PRIMARY KEY (id)
);
COMMENT ON TABLE employee is '社員情報';
COMMENT ON COLUMN employee.id IS 'ID';
COMMENT ON COLUMN employee.name IS '名前';
COMMENT ON COLUMN employee.age IS '年齢';
COMMENT ON COLUMN employee.hire_date IS '入社日';
COMMENT ON COLUMN employee.enrollment IS '在籍';
COMMENT ON COLUMN employee.version IS 'バージョン';

 

 

 

1.2.シーケンス

-- 100から開始
CREATE SEQUENCE employee_seq START 100;

 

 

 

2.プロジェクトのソースコードの構成

src/main/java
└ com.example
  └ doma2
    ├ AppConfig.java
    ├ dao
    │  └ EmployeeDao.java
    └ entity
       └ Employee.java

src/main/resuorce
└ MATA-INF
  └ com
    └ example
      └ doma2
        └ dao
          └ EmployeeDao
            ├ selectAll.sql
            └ selectById.sql

3.AppConfig.java

@SingletonConfig
public class AppConfig implements Config {

    private static final AppConfig CONFIG = new AppConfig();

    private final Dialect dialect;

    private final LocalTransactionDataSource dataSource;

    private final TransactionManager transactionManager;

    private AppConfig() {
        dialect = new PostgresDialect();
        dataSource = new LocalTransactionDataSource(
                "jdbc:postgresql://localhost:5432/sample",
                "postgres", "postgres");
        transactionManager = new LocalTransactionManager(
                dataSource.getLocalTransaction(getJdbcLogger()));
    }

    @Override
    public Dialect getDialect() {
        return dialect;
    }

    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @Override
    public TransactionManager getTransactionManager() {
        return transactionManager;
    }

    public static AppConfig singleton() {
        return CONFIG;
    }
}

 

 

 

4.EmployeeDao.java

@Dao(config = AppConfig.class)
public interface EmployeeDao {
    @Select
    List<Employee> selectAll();

    @Select
    Employee selectById(Integer id);

    @Insert
    int insert(Employee employee);

    @Update
    int update(Employee employee);

    @Delete
    int delete(Employee employee);
}

 

 

 

5.Employee.java

@Entity(naming = NamingType.SNAKE_UPPER_CASE)
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @SequenceGenerator(sequence = "EMPLOYEE_SEQ")
    private Integer id;

    private String name;

    private Integer age;

    private LocalDate hireDate;

    private Boolean enrollment;

    @Version
    private Integer version;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public LocalDate getHireDate() {
        return hireDate;
    }

    public void setHireDate(LocalDate hireDate) {
        this.hireDate = hireDate;
    }

    public Boolean getEnrollment() {
        return enrollment;
    }

    public void setEnrollment(Boolean enrollment) {
        this.enrollment = enrollment;
    }

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }

}

 

 

 

6.selectAll.sql

select /*%expand*/* from employee order by id

 

 

 

7.selectById.sql

select /*%expand*/* from employee where id = /* id */0

 

 

 

以上がテスト対象のコードになります。

環境構築 ~DbSetupとAssertJ-DB~

1.Mavenの追記

依存関係の追加
DbSetupとAssertJ-DBの依存関係を、追加します。

  <dependency>
    <groupId>com.ninja-squad</groupId>
    <artifactId>DbSetup</artifactId>
    <version>2.1.0</version>
  </dependency>
  <dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-db</artifactId>
    <version>1.2.0</version>
  </dependency>

 

Mavenを更新して、必要なjarをローカルリポジトリにダウンロードします。

テストコードの作成

1.プロジェクトのソースコードの構成

src/main/test
└ com.example
  └ doma2.dao
    └ EmployeeDaoTest.java

2.EmployeeDaoTest.java

本クラスに、事前に投入するデータベースの初期データや
各サンプルコードのテストコードを実装しています。

以下、用途毎順に説明していきます。

2.1 初期データの投入

初期データを準備するためには、Operationクラスを使用します。
今回は、以下の例のように対象のテーブルを全件削除するOperationと
対象のテーブルに挿入するデータを定義したOperationを作成します。

定義した操作を実行する場合はDbSetupに、
DBの接続情報を持たせたDataDestinationと、
先程定義したOperationを引数に渡して、オブジェクトを作成します。
作成したDbSetupオブジェクトのlaunchを呼び出すことで、実行できます。

    // 全件削除
    private static final Operation DELETE_ALL = Operations.deleteAllFrom("employee");

    // 初期データ
    private static final Operation INSERT = Operations.insertInto("employee")
                .columns("id", "name", "age", "hire_date" ,"enrollment", "version")
                .values(1, "佐藤", 22, LocalDate.of(2018, 4, 1), true, 1)
                .values(2, "鈴木", 31, LocalDate.of(2014, 12, 1), true, 1)
                .values(3, "高橋", 32, LocalDate.of(2015, 1, 1), false, 1)
                .build();

    @Before
    public void before() {
        Destination dest = new DataSourceDestination(AppConfig.singleton().getDataSource());
        DbSetup setup = new DbSetup(dest, Operations.sequenceOf(DELETE_ALL, INSERT));
            TransactionManager tm = AppConfig.singleton().getTransactionManager();
        tm.required(() -> {
            setup.launch();
        });
    }

 

 

 

2.2 初期データの投入の確認

実際に2.1.で投入した、初期データの確認をしてみましょう。

AssertJ-DBには、Tableというクラスがあります。

このクラスを使用することで、

SQLを使用しなくてもテーブルの内容を確認することができます。

※ここで1点注意することがあります。
 Java8から追加された、LocalDate,LocalTime,LocalDateTimeを
 AssertJ-DBでテストする場合、
 そのまま利用しても不一致と判断されエラーとなってしまいます。

上記クラスのテストを行う場合は、代わりにDateValue,TimeValue,DateTimeValueが
用意されていますので、こちらのクラスを利用する必要があります。

以下の例が、employeeテーブルの件数と値を検証するコードになります。

    @Test
    public void setupTest() {
        TransactionManager tm = AppConfig.singleton().getTransactionManager();
        tm.required(() -> {
            Table table = new Table(AppConfig.singleton().getDataSource(), "employee");

            // 件数の確認
            assertThat(table).column().hasNumberOfRows(3);

            // 値の確認
            assertThat(table)
                .row(0).hasValues(1, "佐藤", 22, DateValue.of(2018, 4, 1), true, 1)
                .row(1).hasValues(2, "鈴木", 31, DateValue.of(2014, 12, 1), true, 1)
                .row(2).hasValues(3, "高橋", 32, DateValue.of(2015, 1, 1), false, 1);

        });
    }

 

 

 

メニューバーの実行(R)から、実行(S) → Junit を選択して実行してみます。
以下の通り、結果が成功になっていることがわかります。

2.3.データの検索(select)の確認

データベースの検索を確認する場合、
データベースのデータの差分を確認する必要がない為、
取得したデータに対してアサートを行います。

    // テスト対象クラス
    private EmployeeDao dao = new EmployeeDaoImpl();

    @Test
    public void testSelect() {
        TransactionManager tm = AppConfig.singleton().getTransactionManager();
        tm.required(() -> {
            Employee employee = dao.selectById(1);
            assertThat(employee.getId(), is(1));
            assertThat(employee.getName(), is("佐藤"));
            assertThat(employee.getAge(), is(22));
            assertThat(employee.getHireDate(), is(LocalDate.of(2018, 4, 1)));
            assertTrue(employee.getEnrollment());
            assertThat(employee.getVersion(), is(1));
        });
    }

 

 

 

2.4.データの挿入(Insert)の確認

データベースの差分を確認するには、Changesクラスを使用します。
Changesはスタート時点とエンド時点を定義することで、
その間のデータベースに対する変更を確認することが出来ます。

Changesを利用してデータ挿入のアサートをする場合、
isCreationを実行することで確認することができます。

また、rowAtEndPointを実行することでエンド時点でのデータを確認することでができます。

    // テスト対象クラス
    private EmployeeDao dao = new EmployeeDaoImpl();

    @Test
    public void testInsert() throws Exception {

        Changes changes = new Changes(AppConfig.singleton().getDataSource());

        TransactionManager tm = AppConfig.singleton().getTransactionManager();
        tm.required(() -> {
            changes.setStartPointNow();

            Employee employee = new Employee();
            employee.setId(4);
            employee.setName("田中");
            employee.setAge(25);
            employee.setHireDate(LocalDate.of(2018, 6, 27));
            employee.setEnrollment(true);
            employee.setVersion(1);

            dao.insert(employee);

            changes.setEndPointNow();
            assertThat(changes)
                .hasNumberOfChanges(1)
                .change()
                .isCreation()
                .isOnTable("employee")
                .rowAtEndPoint()
                    .hasValues(4, "田中", 25, DateValue.of(2018, 6, 27), true, 1);
        });
    }

 

HasValuesを利用して想定値を書くと、
カラムを指定しなくて良いので記述が少なくスッキリしていて良いのですが、
シーケンスなどで値が採番される場合、値が不確定の為使用できません。

そのような場合はカラムを指定してアサートすることも可能です。

また、以下のように値の完全一致だけでなく、
ヌルチェックや想定値との比較などの確認ができます。

    // テスト対象クラス
    private EmployeeDao dao = new EmployeeDaoImpl();

    @Test
    public void testInsert2() throws Exception {

        Changes changes = new Changes(AppConfig.singleton().getDataSource());

        TransactionManager tm = AppConfig.singleton().getTransactionManager();
        tm.required(() -> {
            changes.setStartPointNow();

            Employee employee = new Employee();
            //IDとVersionは自動設定
            employee.setName("田中");
            employee.setAge(null);
            employee.setHireDate(LocalDate.of(2018, 6, 27));
            employee.setEnrollment(true);

            dao.insert(employee);

            changes.setEndPointNow();
            assertThat(changes)
                .hasNumberOfChanges(1)
                .change()
                .isCreation()
                .isOnTable("employee")
                .rowAtEndPoint()
                    .value("id").isNumber()
                    .value("name").isEqualTo("田中")
                    .value("age").isNull()
                    .value("hire_date").isAfter(DateValue.of(2018, 6, 26))
                    .value("enrollment").isTrue()
                    .value("version").isGreaterThan(0);
        });
    }

 

 

 

2.5.データの更新(Update)の確認

データの更新を確認するにも、Changesクラスを使用します。
データ更新のアサートをする場合、
isModificationを実行することで確認することができます。

データ更新の場合は、スタート時点のデータが確認できるrowAtStartPointと
エンド時点のrowAtEndPointを実行することで、より正確にアサートを行えます。

 

    // テスト対象クラス
    private EmployeeDao dao = new EmployeeDaoImpl();

    @Test
    public void testUpdate() throws Exception {

        Changes changes = new Changes(AppConfig.singleton().getDataSource());

        TransactionManager tm = AppConfig.singleton().getTransactionManager();
        tm.required(() -> {
            changes.setStartPointNow();
            Employee employee = dao.selectById(1);
            employee.setName("山田");
            dao.update(employee);
            changes.setEndPointNow();
            assertThat(changes)
                .hasNumberOfChanges(1)
                .change()
                .isModification()
                .isOnTable("employee")
                .rowAtStartPoint()
                    .hasValues(1, "佐藤", 22, DateValue.of(2018, 4, 1), true, 1)
                .rowAtEndPoint()
                    .hasValues(1, "山田", 22, DateValue.of(2018, 4, 1), true, 2);
        });
    }

 

 

 

2.6.データの削除(Delete)の確認

データ削除のアサートをする場合、
isDeletionを実行することで確認することができます。

    // テスト対象クラス
    private EmployeeDao dao = new EmployeeDaoImpl();

    @Test
    public void testIDelete() throws Exception {

        Changes changes = new Changes(AppConfig.singleton().getDataSource());

        TransactionManager tm = AppConfig.singleton().getTransactionManager();
        tm.required(() -> {
            changes.setStartPointNow();

            Employee employee = dao.selectById(1);
            dao.delete(employee);

            changes.setEndPointNow();
            assertThat(changes)
                .hasNumberOfChanges(1)
                .change()
                .isDeletion()
                .isOnTable("employee")
                .rowAtStartPoint()
                    .value("id").isEqualTo(1);
        });
    }

 

 

 

まとめ

DbSetupを用いることで、初期データを全てJavaで書くことが出来ました。

初期データをコードで書くのは少し手間な部分はありますが、
DBUnitのように別途ファイルを用意する必要が無いので、管理コストの削減ができます。

テーブルに修正が入った場合、当然テストクラスも変更が必要です。

特にExcelでテストデータを作成していた場合、
ファイル内のデータまでは検索で引っかからない為、修正対象を探すのも手間です。

その点、Javaで書いていれば検索で引っ掛けることが可能ですし、
修正した差分を確認したい場合も容易となります。

AssertJ-DBに関しても最初慣れが必要ですが、DBの差分を簡単に確認できるので非常に便利です。

藁科
CSVIT事業部 LS(リーディング・サービス)部 藁科
最後までお読みいただき、ありがとうございます!是非、機会がありましたらご活用ください。