CLOVER🍀

That was when it all began.

データベースのデータに対するアサーションができるAssertJ-DBを試してみる

これは、なにをしたくて書いたもの?

AssertJのモジュールのひとつである、データベースのデータに対するアサーションができるAssertJ-DBを試してみようかなということで。

AssertJ-DB

AssertJ-DBのドキュメントは、AssertJのドキュメントの中に含まれています。

AssertJ - fluent assertions java library

こちらですね。

AssertJ - fluent assertions java library / AssertJ DB

GitHubリポジトリーはこちら。

GitHub - assertj/assertj-db: Assertions for database

独立したリポジトリーになっています。現在のバージョンは3.0.0です。

ざっくり言うと、以下のような感じです。

  • データの取得元はテーブル、リクエスト(SQL)、一定期間の変更内容から選択する
    • Table、Request、Changes
  • 対象の行や列を選択する
  • 各種データ型に対してアサーションを行う

サンプルコードはこちらにあります。

https://github.com/assertj/assertj-examples/tree/main/assertions-examples/src/test/java/org/assertj/examples/db

たぶん、実際に使うのはChangesだと思うのですが、慣れるためにTableRequestの順に見ていった方がよい気がします。
説明を続けるよりも実際に見ていった方が早いかなと思うので、試していってみましょう。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu124.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu124.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.5, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-49-generic", arch: "amd64", family: "unix"

データベースにはMySQLを使います。

 MySQL  localhost:3306 ssl  practice  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.4.3     |
+-----------+
1 row in set (0.0006 sec)

MySQLには172.17.0.2でアクセスできるものとします。

お題

書籍とそのカテゴリーをお題に、AssertJ-DBを使ってアサーションをしてみたいと思います。

create table if not exists category(
  id integer,
  name varchar(100),
  primary key(id)
);


create table if not exists book(
  isbn varchar(14),
  title varchar(100),
  price int,
  publish_date date,
  category_id integer,
  primary key(isbn),
  foreign key(category_id) references category(id)
);

準備

Maven依存関係など。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>9.1.0</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma-core</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.11.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-db</artifactId>
            <version>3.0.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.seasar.doma</groupId>
                            <artifactId>doma-processor</artifactId>
                            <version>3.1.0</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

データベースアクセスにはDomaを使うことにします。

AssertJ-DBを使うのに必要な依存関係はこちらだけですね。

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-db</artifactId>
            <version>3.0.0</version>
            <scope>test</scope>
        </dependency>

AssertJ-DBはAssertJ-Coreに依存しています。

テストコード以外のものも用意していきましょう。

エンティティクラスとDao。

src/main/java/org/littlewings/assertjdb/Category.java

package org.littlewings.assertjdb;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public record Category(
        @Id
        Integer id,
        String name
) {
}

src/main/java/org/littlewings/assertjdb/CategoryDao.java

package org.littlewings.assertjdb;

import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Script;
import org.seasar.doma.Sql;
import org.seasar.doma.Update;
import org.seasar.doma.jdbc.Result;

@Dao
public interface CategoryDao {
    @Sql("""
            create table if not exists category(
              id integer,
              name varchar(100),
              primary key(id)
            )
            """)
    @Script
    void createTableIfNotExists();

    @Insert
    Result<Category> insert(Category category);

    @Update
    Result<Category> update(Category category);

    @Delete
    Result<Category> delete(Category category);

    @Sql("delete from category")
    @Delete
    int deleteAll();
}

src/main/java/org/littlewings/assertjdb/Book.java

package org.littlewings.assertjdb;

import java.time.LocalDate;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public record Book(
        @Id
        String isbn,
        String title,
        Integer price,
        LocalDate publishDate,
        Integer categoryId
) {
}

src/main/java/org/littlewings/assertjdb/BookDao.java

package org.littlewings.assertjdb;

import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Script;
import org.seasar.doma.Sql;
import org.seasar.doma.Update;
import org.seasar.doma.jdbc.Result;

@Dao
public interface BookDao {
    @Sql("""
            create table if not exists book(
              isbn varchar(14),
              title varchar(100),
              price int,
              publish_date date,
              category_id integer,
              primary key(isbn),
              foreign key(category_id) references category(id)
            )
            """)
    @Script
    void createTableIfNotExists();

    @Insert
    Result<Book> insert(Book book);

    @Update
    Result<Book> update(Book book);

    @Delete
    Result<Book> delete(Book book);

    @Sql("delete from book")
    @Delete
    int deleteAll();
}

簡単のため、DDLはDaoのメソッドのひとつとして用意することにしました。

Domaの設定。

src/main/java/org/littlewings/assertjdb/DomaConfig.java

package org.littlewings.assertjdb;

import javax.sql.DataSource;

import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.Naming;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.MysqlDialect;
import org.seasar.doma.jdbc.tx.LocalTransactionDataSource;
import org.seasar.doma.jdbc.tx.LocalTransactionManager;
import org.seasar.doma.jdbc.tx.TransactionManager;

public class DomaConfig implements Config {
    private static final DomaConfig CONFIG = new DomaConfig();

    private Dialect dialect;

    private LocalTransactionDataSource dataSource;

    private TransactionManager transactionManager;

    private DomaConfig() {
        dialect = new MysqlDialect();
        dataSource = new LocalTransactionDataSource(
                "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                "kazuhira",
                "password"
        );
        transactionManager = new LocalTransactionManager(dataSource.getLocalTransaction(getJdbcLogger()));
    }

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

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

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

    @Override
    public Naming getNaming() {
        return Naming.SNAKE_LOWER_CASE;
    }

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

テストコードの雛形。

src/test/java/org/littlewings/assertjdb/AssertJDbTest.java

package org.littlewings.assertjdb;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;

import org.assertj.db.type.AssertDbConnection;
import org.assertj.db.type.AssertDbConnectionFactory;
import org.assertj.db.type.Changes;
import org.assertj.db.type.DateValue;
import org.assertj.db.type.Request;
import org.assertj.db.type.Table;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.db.api.Assertions.assertThat;
import static org.assertj.db.output.Outputs.output;

public class AssertJDbTest {
    @BeforeAll
    static void setUpAll() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    categoryDao.createTableIfNotExists();
                    bookDao.createTableIfNotExists();
                });
    }

    @BeforeEach
    void setUp() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    bookDao.deleteAll();
                    categoryDao.deleteAll();
                });
    }

    // ここにテストを書く!
}

テスト実行前にテーブルを作成し、テストの都度データをすべて削除するようにしています。

準備はここまでにして、AssertJ-DBを使っていってみましょう。

Tableを使う

まずはTableを使ってみましょう。

Getting Started的な

作成したテストコードはこちら。

    @Test
    void gettingStarted() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Map<String, Category> categories = Map.of(
                            "Java", new Category(1, "Java"),
                            "MySQL", new Category(2, "MySQL")
                    );

                    List<Book> books = List.of(
                            new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), categories.get("Java").id()),
                            new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), categories.get("Java").id()),
                            new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), categories.get("Java").id()),
                            new Book("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, LocalDate.of(2024, 5, 22), categories.get("MySQL").id()),
                            new Book("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), categories.get("MySQL").id())
                    );

                    categories.values().forEach(categoryDao::insert);
                    books.forEach(bookDao::insert);
                });

        AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(
                        "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                        "kazuhira",
                        "password"
                )
                .create();
        //AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

        Table categoryTable = assertDbConnection.table("category").build();
        assertThat(categoryTable)
                .hasNumberOfRows(2)
                .column("id")
                .value().isEqualTo(1)
                .value().isEqualTo(2)
                .column("name")
                .value().isEqualTo("Java")
                .value().isEqualTo("MySQL");

        Table bookTable = assertDbConnection.table("book").build();
        assertThat(bookTable)
                .hasNumberOfRows(5)
                .column("title")
                .value().isEqualTo("MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ")
                .value().isEqualTo("Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~")
                .value().isEqualTo("Effective Java 第3版")
                .value().isEqualTo("Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで")
                .value().isEqualTo("MySQL徹底入門 第4版 MySQL 8.0対応")
                .column("publish_date")
                .hasValues(
                        // この順番で
                        DateValue.from(LocalDate.of(2024, 5, 22)),
                        DateValue.from(LocalDate.of(2024, 10, 3)),
                        DateValue.from(LocalDate.of(2018, 10, 30)),
                        DateValue.from(LocalDate.of(2017, 4, 18)),
                        DateValue.from(LocalDate.of(2020, 7, 6))
                )
                .column("category_id")
                .hasValues(2, 1, 1, 1, 2);
    }

最初の部分はデータを登録しているだけなので端折ります。

AssertJ-DBを使う時に最初に行うのは、AssertDbConnectionの作成です。これはJDBCの接続パラメーターかDataSourceのどちらかを
指定して作成することができます。

        AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(
                        "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                        "kazuhira",
                        "password"
                )
                .create();
        //AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

AssertJ - fluent assertions java library / AssertJ DB / Concepts / Connection to the database

今回はJDBCの接続パラメーターを与えていますが、実際に使う時にはDataSourceを指定することが多いような気がします。
以降はDataSourceを指定することにします。

なお、データベース内のメタデータを取得するようなのですが、テーブル数が多い場合などは1度だけ取得するように設定した方が
よさそうです。

AssertJ - fluent assertions java library / AssertJ DB / Concepts / Connection to the database / Schema retrieval mode

次にAssertDbConnectionからTableを取得します。

        Table categoryTable = assertDbConnection.table("category").build();

        Table bookTable = assertDbConnection.table("book").build();

Tableは文字通りデータベースのテーブルを表したものです。

AssertJ - fluent assertions java library / AssertJ DB / Concepts / Elements of the database / Table

アサーションの例。

最初にカラム名を指定すると、カラム → 行の順でアサーションしていくことになるようです。hasNumberOfRowsはテーブルの行数ですね。

        assertThat(categoryTable)
                .hasNumberOfRows(2)
                .column("id")
                .value().isEqualTo(1)
                .value().isEqualTo(2)
                .column("name")
                .value().isEqualTo("Java")
                .value().isEqualTo("MySQL");

カラムひとつずつではなく、hasValuesを使うとその行に含まれるすべてのカラムをまとめてアサーションすることもできます。

        assertThat(bookTable)
                .hasNumberOfRows(5)
                .column("title")
                .value().isEqualTo("MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ")
                .value().isEqualTo("Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~")
                .value().isEqualTo("Effective Java 第3版")
                .value().isEqualTo("Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで")
                .value().isEqualTo("MySQL徹底入門 第4版 MySQL 8.0対応")
                .column("publish_date")
                .hasValues(
                        // この順番で
                        DateValue.from(LocalDate.of(2024, 5, 22)),
                        DateValue.from(LocalDate.of(2024, 10, 3)),
                        DateValue.from(LocalDate.of(2018, 10, 30)),
                        DateValue.from(LocalDate.of(2017, 4, 18)),
                        DateValue.from(LocalDate.of(2020, 7, 6))
                )
                .column("category_id")
                .hasValues(2, 1, 1, 1, 2);

データは主キーの昇順でソートされているようです。

アサーションについてはこちら。

AssertJ - fluent assertions java library / AssertJ DB / Features highlight / Navigation / Assertions

ちなみに、日付型はLocalDateで扱えるはずなのですがDateValueに変換しているのは使っているMySQLの都合だと思います…。

行でナビゲーションする

個人的にはカラム → 行の順はちょっとわかりにくい気がするので、行 → カラムで見れるようにしてみましょう。

    @Test
    void orderByAndRowNavigation() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Map<String, Category> categories = Map.of(
                            "Java", new Category(1, "Java"),
                            "MySQL", new Category(2, "MySQL")
                    );

                    List<Book> books = List.of(
                            new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), categories.get("Java").id()),
                            new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), categories.get("Java").id()),
                            new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), categories.get("Java").id()),
                            new Book("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, LocalDate.of(2024, 5, 22), categories.get("MySQL").id()),
                            new Book("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), categories.get("MySQL").id())
                    );

                    categories.values().forEach(categoryDao::insert);
                    books.forEach(bookDao::insert);
                });

        AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

        Table categoryTable = assertDbConnection.table("category").columnsToOrder(new Table.Order[]{Table.Order.asc("id")}).build();
        assertThat(categoryTable)
                .hasNumberOfRows(2)
                .row()
                .column("id")
                .value().isEqualTo(1)
                .column("name")
                .value().isEqualTo("Java")
                .row()
                .column("id")
                .value().isEqualTo(2)
                .column("name")
                .value().isEqualTo("MySQL");

        Table bookTable = assertDbConnection.table("book").columnsToOrder(new Table.Order[]{Table.Order.asc("price")}).build();
        assertThat(bookTable)
                .hasNumberOfRows(5)
                .row()
                .hasValues(
                        // 取得した全カラムを並べる
                        "978-4297141844",
                        "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
                        3080,
                        DateValue.from(LocalDate.of(2024, 5, 22)),
                        2
                )
                .row()
                .hasValues(
                        "978-4774189093",
                        "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで",
                        3278,
                        DateValue.from(LocalDate.of(2017, 4, 18)),
                        1
                )
                .row()
                .hasValues(
                        "978-4297144357",
                        "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~",
                        3520,
                        DateValue.from(LocalDate.of(2024, 10, 3)),
                        1
                )
                .row()
                .hasValues(
                        "978-4798161488",
                        "MySQL徹底入門 第4版 MySQL 8.0対応",
                        4180,
                        DateValue.from(LocalDate.of(2020, 7, 6)),
                        2
                )
                .row()
                .hasValues(
                        "978-4621303252",
                        "Effective Java 第3版",
                        4400,
                        DateValue.from(LocalDate.of(2018, 10, 30)),
                        1
                );
    }

AssertDbConnectionの取得はDataSourceからにしました。

        AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

また、テーブルのソート順も指定するようにしました。

        Table categoryTable = assertDbConnection.table("category").columnsToOrder(new Table.Order[]{Table.Order.asc("id")}).build();


        Table bookTable = assertDbConnection.table("book").columnsToOrder(new Table.Order[]{Table.Order.asc("price")}).build();

行でナビゲーションするには、rowを使います。行ごとに、カラムを指定してアサーション

        assertThat(categoryTable)
                .hasNumberOfRows(2)
                .row()
                .column("id")
                .value().isEqualTo(1)
                .column("name")
                .value().isEqualTo("Java")
                .row()
                .column("id")
                .value().isEqualTo(2)
                .column("name")
                .value().isEqualTo("MySQL");

rowにインデックスを指定することで、指定の行へ移動することもできるようです。

行の内容をまとめてアサーション

        assertThat(bookTable)
                .hasNumberOfRows(5)
                .row()
                .hasValues(
                        // 取得した全カラムを並べる
                        "978-4297141844",
                        "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
                        3080,
                        DateValue.from(LocalDate.of(2024, 5, 22)),
                        2
                )
                .row()
                .hasValues(
                        "978-4774189093",
                        "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで",
                        3278,
                        DateValue.from(LocalDate.of(2017, 4, 18)),
                        1
                )
                .row()
                .hasValues(
                        "978-4297144357",
                        "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~",
                        3520,
                        DateValue.from(LocalDate.of(2024, 10, 3)),
                        1
                )
                .row()
                .hasValues(
                        "978-4798161488",
                        "MySQL徹底入門 第4版 MySQL 8.0対応",
                        4180,
                        DateValue.from(LocalDate.of(2020, 7, 6)),
                        2
                )
                .row()
                .hasValues(
                        "978-4621303252",
                        "Effective Java 第3版",
                        4400,
                        DateValue.from(LocalDate.of(2018, 10, 30)),
                        1
                );

AssertJ - fluent assertions java library / AssertJ DB / Features highlight / Navigation / With a Table or a Request as root / To a Row

その他のTableに対するナビゲーションについてはこちらを参照。

AssertJ - fluent assertions java library / AssertJ DB / Features highlight / Navigation / With a Table or a Request as root

Tableの内容を出力する

Tableの内容を出力するには、outputを使います。

    @Test
    void tableOutput() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Map<String, Category> categories = Map.of(
                            "Java", new Category(1, "Java"),
                            "MySQL", new Category(2, "MySQL")
                    );

                    List<Book> books = List.of(
                            new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), categories.get("Java").id()),
                            new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), categories.get("Java").id()),
                            new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), categories.get("Java").id()),
                            new Book("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, LocalDate.of(2024, 5, 22), categories.get("MySQL").id()),
                            new Book("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), categories.get("MySQL").id())
                    );

                    categories.values().forEach(categoryDao::insert);
                    books.forEach(bookDao::insert);
                });

        AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

        Table categoryTable = assertDbConnection.table("category").columnsToOrder(new Table.Order[]{Table.Order.asc("id")}).build();
        Table bookTable = assertDbConnection.table("book").columnsToOrder(new Table.Order[]{Table.Order.asc("price")}).build();

        output(categoryTable).toConsole();
        output(bookTable).toConsole();
    }

結果はこんな感じになります。

[category table]
|-----------|---------|-----------|-----------|
|           |         | *         |           |
|           | PRIMARY | ID        | NAME      |
|           | KEY     | (NUMBER)  | (TEXT)    |
|           |         | Index : 0 | Index : 1 |
|-----------|---------|-----------|-----------|
| Index : 0 | 1       | 1         | Java      |
| Index : 1 | 2       | 2         | MySQL     |
|-----------|---------|-----------|-----------|
[book table]
|-----------|----------------|----------------|----------------------------------------------------|-----------|--------------|-------------|
|           |                | *              |                                                    |           |              |             |
|           | PRIMARY        | ISBN           | TITLE                                              | PRICE     | PUBLISH_DATE | CATEGORY_ID |
|           | KEY            | (TEXT)         | (TEXT)                                             | (NUMBER)  | (DATE)       | (NUMBER)    |
|           |                | Index : 0      | Index : 1                                          | Index : 2 | Index : 3    | Index : 4   |
|-----------|----------------|----------------|----------------------------------------------------|-----------|--------------|-------------|
| Index : 0 | 978-4297141844 | 978-4297141844 | MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ        | 3080      | 2024-05-22   | 2           |
| Index : 1 | 978-4774189093 | 978-4774189093 | Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで         | 3278      | 2017-04-18   | 1           |
| Index : 2 | 978-4297144357 | 978-4297144357 | Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~ | 3520      | 2024-10-03   | 1           |
| Index : 3 | 978-4798161488 | 978-4798161488 | MySQL徹底入門 第4版 MySQL 8.0対応                          | 4180      | 2020-07-06   | 2           |
| Index : 4 | 978-4621303252 | 978-4621303252 | Effective Java 第3版                                 | 4400      | 2018-10-30   | 1           |
|-----------|----------------|----------------|----------------------------------------------------|-----------|--------------|-------------|

AssertJ - fluent assertions java library / AssertJ DB / Concepts / Output

現在の状態がよくわかって便利なのですが、これが表示できるのはTableのみのようです。

この後に出てくるRequestChangesでもメソッド自体は呼べるのですが、なにも表示されませんでした…。

カラムを除外する

Tableを取得する際に、アサーション対象のカラムを除外できます。

    @Test
    void columnsExclude() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Map<String, Category> categories = Map.of(
                            "Java", new Category(1, "Java"),
                            "MySQL", new Category(2, "MySQL")
                    );

                    List<Book> books = List.of(
                            new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), categories.get("Java").id()),
                            new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), categories.get("Java").id()),
                            new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), categories.get("Java").id()),
                            new Book("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, LocalDate.of(2024, 5, 22), categories.get("MySQL").id()),
                            new Book("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), categories.get("MySQL").id())
                    );

                    categories.values().forEach(categoryDao::insert);
                    books.forEach(bookDao::insert);
                });

        AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

        Table bookTable =
                assertDbConnection
                        .table("book")
                        .columnsToOrder(new Table.Order[]{Table.Order.asc("price")})
                        .columnsToExclude(new String[]{"isbn", "publish_date", "category_id"})
                        .build();
        assertThat(bookTable)
                .hasNumberOfRows(5)
                .row()
                .hasValues(
                        "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
                        3080
                )
                .row()
                .hasValues(
                        "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで",
                        3278
                )
                .row()
                .hasValues(
                        "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~",
                        3520
                )
                .row()
                .hasValues(
                        "MySQL徹底入門 第4版 MySQL 8.0対応",
                        4180
                )
                .row()
                .hasValues(
                        "Effective Java 第3版",
                        4400
                );
    }

この部分ですね。

        Table bookTable =
                assertDbConnection
                        .table("book")
                        .columnsToOrder(new Table.Order[]{Table.Order.asc("price")})
                        .columnsToExclude(new String[]{"isbn", "publish_date", "category_id"})
                        .build();

hasValuesでは取得したすべてのカラムの値を列挙する必要がありますが、除外したカラムについては対象外になります。

        assertThat(bookTable)
                .hasNumberOfRows(5)
                .row()
                .hasValues(
                        "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
                        3080
                )

AssertJ - fluent assertions java library / AssertJ DB / Concepts / Elements of the database / Table

Request

Requestは、SQLを使ってデータを取得した結果を表したものです。

    @Test
    void request() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Map<String, Category> categories = Map.of(
                            "Java", new Category(1, "Java"),
                            "MySQL", new Category(2, "MySQL")
                    );

                    List<Book> books = List.of(
                            new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), categories.get("Java").id()),
                            new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), categories.get("Java").id()),
                            new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), categories.get("Java").id()),
                            new Book("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, LocalDate.of(2024, 5, 22), categories.get("MySQL").id()),
                            new Book("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), categories.get("MySQL").id())
                    );

                    categories.values().forEach(categoryDao::insert);
                    books.forEach(bookDao::insert);
                });

        AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

        Request categoryRequest = assertDbConnection
                .request("select id, name from category where id = ? order by id")
                .parameters(1)
                .build();
        assertThat(categoryRequest)
                .hasNumberOfRows(1)
                .row()
                .column("id")
                .value().isEqualTo(1)
                .column("name")
                .value().isEqualTo("Java");

        Request bookRequest =
                assertDbConnection
                        .request("select isbn, title, price, publish_date, category_id from book where price > ? and category_id = ? order by price asc")
                        .parameters(4000, 1)
                        .build();
        assertThat(bookRequest)
                .hasNumberOfRows(1)
                .row()
                .hasValues(
                        "978-4621303252",
                        "Effective Java 第3版",
                        4400,
                        DateValue.from(LocalDate.of(2018, 10, 30)),
                        1
                );
    }

こんな感じで、データの取得方法がSQLになります。バインドパラメーターも使えます。

        Request categoryRequest = assertDbConnection
                .request("select id, name from category where id = ? order by id")
                .parameters(1)
                .build();


        Request bookRequest =
                assertDbConnection
                        .request("select isbn, title, price, publish_date, category_id from book where price > ? and category_id = ? order by price asc")
                        .parameters(4000, 1)
                        .build();

AssertJ - fluent assertions java library / AssertJ DB / Concepts / Elements of the database / Request

それ以外の使い方は、Tableと変わりません。

Changes

Changesは開始時点と終了時点の2点の差をとったものです。

Changesおよびその中にある変更のひとつひとつであるChangeがどう表現されるかがこちらに書いてあります。

まあ、最初は流し読みしてしまうと思うのですが、これが頭に入っていないとアサーションが書けません…。

先に例を載せましょう。

    @Test
    void changes() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Category javaCategory = new Category(1, "Java");

                    List<Book> javaBooks = List.of(
                            new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), javaCategory.id()),
                            new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), javaCategory.id()),
                            new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), javaCategory.id()),
                            new Book("978-4798151120", "独習Java 新版", 3278, LocalDate.of(2019, 5, 15), javaCategory.id())
                    );

                    categoryDao.insert(javaCategory);
                    javaBooks.forEach(bookDao::insert);

                    AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

                    Changes changes = assertDbConnection.changes().build();
                    // 開始
                    changes.setStartPointNow();

                    Category mysqlCategory = new Category(2, "MySQL");

                    List<Book> mysqlBooks = List.of(
                            new Book("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, LocalDate.of(2024, 5, 22), mysqlCategory.id()),
                            new Book("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), mysqlCategory.id())
                    );

                    // insert
                    categoryDao.insert(mysqlCategory);
                    mysqlBooks.forEach(bookDao::insert);
                    // update
                    bookDao.update(new Book("978-4621303252", "Effective Java", 3000, LocalDate.of(2018, 10, 30), javaCategory.id()));
                    bookDao.update(new Book("978-4774189093", "Java本格入門", 2500, LocalDate.of(2017, 4, 18), javaCategory.id()));
                    // delete
                    bookDao.delete(new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), javaCategory.id()));

                    // 終了
                    changes.setEndPointNow();

                    assertThat(changes)
                            // 全体の変更数
                            .hasNumberOfChanges(6)
                            // 種類ごとの変更数
                            .ofCreation().hasNumberOfChanges(3)
                            .ofModification().hasNumberOfChanges(2)
                            .ofDeletion().hasNumberOfChanges(1)
                            // categoryテーブルの変更数
                            .onTable("category")
                            .hasNumberOfChanges(1)
                            // bookテーブルの変更数
                            .onTable("book")
                            .hasNumberOfChanges(5);

                    assertThat(changes)
                            // ひとつ目の変更(insert)
                            .change()
                            .isOnTable("book")
                            .isCreation()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4297141844")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .doesNotExist()
                            // 終了時点の行
                            .rowAtEndPoint()
                            .hasValues("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, DateValue.from(LocalDate.of(2024, 5, 22)), 2)
                            // 2つ目の変更(insert)
                            .change()
                            .isOnTable("book")
                            .isCreation()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4798161488")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .doesNotExist()
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("isbn").isEqualTo("978-4798161488")
                            .value("title").isEqualTo("MySQL徹底入門 第4版 MySQL 8.0対応")
                            .value("price").isEqualTo(4180)
                            .value("publish_date").isEqualTo(DateValue.from(LocalDate.of(2020, 7, 6)))
                            .value("category_id").isEqualTo(2)
                            // 3つ目の変更(insert)
                            .change()
                            .isOnTable("category")
                            .isCreation()
                            .hasPksNames("id")
                            .hasPksValues(2)
                            // 開始時点の行
                            .rowAtStartPoint()
                            .doesNotExist()
                            .rowAtEndPoint()
                            .value("id").isEqualTo(2)
                            .value("name").isEqualTo("MySQL")
                            // 4つ目の変更(update)
                            .change()
                            .isOnTable("book")
                            .isModification()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4621303252")
                            // 変更されたカラム
                            .hasModifiedColumns("title", "price")
                            // 変更前後のカラムの値を参照
                            .column("title").hasValues("Effective Java 第3版", "Effective Java")
                            .column("price").hasValues(4400, 3000)
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("title").isEqualTo("Effective Java 第3版")
                            .value("price").isEqualTo(4400)
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("title").isEqualTo("Effective Java")
                            .value("price").isEqualTo(3000)
                            // 5つ目の変更(update)
                            .change()
                            .isOnTable("book")
                            .isModification()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4774189093")
                            .hasModifiedColumns("title", "price")
                            // 変更前後のカラムの値を参照
                            .column("title").hasValues("Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", "Java本格入門")
                            .column("price").hasValues(3278, 2500)
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("title").isEqualTo("Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで")
                            .value("price").isEqualTo(3278)
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("title").isEqualTo("Java本格入門")
                            .value("price").isEqualTo(2500)
                            // 6つ目の変更(delete)
                            .change()
                            .isOnTable("book")
                            .isDeletion()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4297144357")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("isbn").isEqualTo("978-4297144357")
                            .value("title").isEqualTo("Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~")
                            // 終了時点の行
                            .rowAtEndPoint()
                            .doesNotExist();
                });
    }

今回は、データの操作を2つに分けています。

まずはデータの登録。

                .required(() -> {
                    Category javaCategory = new Category(1, "Java");

                    List<Book> javaBooks = List.of(
                            new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), javaCategory.id()),
                            new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), javaCategory.id()),
                            new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), javaCategory.id()),
                            new Book("978-4798151120", "独習Java 新版", 3278, LocalDate.of(2019, 5, 15), javaCategory.id())
                    );

                    categoryDao.insert(javaCategory);
                    javaBooks.forEach(bookDao::insert);

この後にChangesを取得します。

                    AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

                    Changes changes = assertDbConnection.changes().build();

Changesは取得しただけではなにも起こらず、変更を記録するにはChanges#setStartPointNowを呼び出す必要があります。

                    // 開始
                    changes.setStartPointNow();

この後、データの登録と更新、削除を行ってみます。

                    Category mysqlCategory = new Category(2, "MySQL");

                    List<Book> mysqlBooks = List.of(
                            new Book("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, LocalDate.of(2024, 5, 22), mysqlCategory.id()),
                            new Book("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), mysqlCategory.id())
                    );

                    // insert
                    categoryDao.insert(mysqlCategory);
                    mysqlBooks.forEach(bookDao::insert);
                    // update
                    bookDao.update(new Book("978-4621303252", "Effective Java", 3000, LocalDate.of(2018, 10, 30), javaCategory.id()));
                    bookDao.update(new Book("978-4774189093", "Java本格入門", 2500, LocalDate.of(2017, 4, 18), javaCategory.id()));
                    // delete
                    bookDao.delete(new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), javaCategory.id()));

そしてChanges#setEndPointNowを呼び出します。

                    // 終了
                    changes.setEndPointNow();

ここでChanges#setStartPointNowを呼び出してからChanges#setEndPointNowまでの間の変更が検出されることになります。

あとはアサーションです。

まずは変更全体を見てみます。

                    assertThat(changes)
                            // 全体の変更数
                            .hasNumberOfChanges(6)
                            // 種類ごとの変更数
                            .ofCreation().hasNumberOfChanges(3)
                            .ofModification().hasNumberOfChanges(2)
                            .ofDeletion().hasNumberOfChanges(1)
                            // categoryテーブルの変更数
                            .onTable("category")
                            .hasNumberOfChanges(1)
                            // bookテーブルの変更数
                            .onTable("book")
                            .hasNumberOfChanges(5);

6つの変更があり、insert(creation)が3つ、update(modification)が2つ、delete(deletion)がひとつです。
テーブルごとには変更の件数のみがわかりそうです。

AssertJ - fluent assertions java library / AssertJ DB / Features highlight / Navigation / AssertionsOn the number of changes

次は変更ごとのアサーションです。

Changesの中に含まれるChangeは、以下の順で並んでいます。

The changes are ordered :

  • First by the type of the change : creation, modification and after deletion
  • After if it is a change on a table by the name of the table
  • To finish by the values of the primary key and if there are no primary key by the values of the row (for a modification)

AssertJ - fluent assertions java library / AssertJ DB / Concepts / Elements of the database / Changes

つまり、こういうことです。

  • insert、update、deleteで大きく分類
  • 分類内ではテーブル名でソート
  • 行はプライマリーキーの順でソート、プライマリーキーがない場合は行の値すべてでソート

なので最初はinsertに対するアサーションを書くことになります。

                            // ひとつ目の変更(insert)
                            .change()
                            .isOnTable("book")
                            .isCreation()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4297141844")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .doesNotExist()
                            // 終了時点の行
                            .rowAtEndPoint()
                            .hasValues("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, DateValue.from(LocalDate.of(2024, 5, 22)), 2)

こういうアサーションになっています。

  • isOnTableでどのテーブルの変更なのかを確認
  • isCreationでinsertであることを確認
  • hasPksNameshasPksValuesで変更があった行のプライマリーキーのカラム名と値を確認
  • rowAtStartPointChangesの開始時点(Changes#setStartPointNow)では行がなかったことを確認
  • rowAtEndPointChangesの終了時点(Changes#setEndPointNow)での行の値を確認

このように、変更内容ひとつひとつ見ていくことになり、TableRequestアサーション時のナビゲーションの考え方がだいぶ異なります。

ここでchangeを呼び出すことで、次の変更を見ることができます。

                            // 2つ目の変更(insert)
                            .change()
                            .isOnTable("book")
                            .isCreation()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4798161488")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .doesNotExist()
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("isbn").isEqualTo("978-4798161488")
                            .value("title").isEqualTo("MySQL徹底入門 第4版 MySQL 8.0対応")
                            .value("price").isEqualTo(4180)
                            .value("publish_date").isEqualTo(DateValue.from(LocalDate.of(2020, 7, 6)))
                            .value("category_id").isEqualTo(2)
                            // 3つ目の変更(insert)
                            .change()
                            .isOnTable("category")
                            .isCreation()
                            .hasPksNames("id")
                            .hasPksValues(2)
                            // 開始時点の行
                            .rowAtStartPoint()
                            .doesNotExist()
                            .rowAtEndPoint()
                            .value("id").isEqualTo(2)
                            .value("name").isEqualTo("MySQL")

insertの変更を見終えると、次はupdateの変更に移ります。

                            // 4つ目の変更(update)
                            .change()
                            .isOnTable("book")
                            .isModification()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4621303252")
                            // 変更されたカラム
                            .hasModifiedColumns("title", "price")
                            // 変更前後のカラムの値を参照
                            .column("title").hasValues("Effective Java 第3版", "Effective Java")
                            .column("price").hasValues(4400, 3000)
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("title").isEqualTo("Effective Java 第3版")
                            .value("price").isEqualTo(4400)
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("title").isEqualTo("Effective Java")
                            .value("price").isEqualTo(3000)

こういうアサーションになっています。

  • isOnTableでどのテーブルの変更なのかを確認
  • isModificationでupdateであることを確認
  • hasPksNameshasPksValuesで変更があった行のプライマリーキーのカラム名と値を確認
  • hasModifiedColumnsで変更されたカラムを確認
  • columnhasValuesで変更前後のカラムの値を確認
  • rowAtStartPointChangesの開始時点(Changes#setStartPointNow)でのカラムの値を確認
  • rowAtEndPointChangesの終了時点(Changes#setEndPointNow)でのカラムの値を確認

columnhasValuesで行っているアサーションと、rowAtStartPointrowAtEndPointで行っているアサーションは意味としては同じです。
実際に使う時はどちらかでよいでしょう。

この後もひとつupdateが続きます。

                            // 5つ目の変更(update)
                            .change()
                            .isOnTable("book")
                            .isModification()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4774189093")
                            .hasModifiedColumns("title", "price")
                            // 変更前後のカラムの値を参照
                            .column("title").hasValues("Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", "Java本格入門")
                            .column("price").hasValues(3278, 2500)
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("title").isEqualTo("Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで")
                            .value("price").isEqualTo(3278)
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("title").isEqualTo("Java本格入門")
                            .value("price").isEqualTo(2500)

最後はdeleteです。

                            // 6つ目の変更(delete)
                            .change()
                            .isOnTable("book")
                            .isDeletion()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4297144357")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("isbn").isEqualTo("978-4297144357")
                            .value("title").isEqualTo("Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~")
                            // 終了時点の行
                            .rowAtEndPoint()
                            .doesNotExist();

こういうアサーションになっています。

  • isOnTableでどのテーブルの変更なのかを確認
  • isDeletionでdeleteであることを確認
  • hasPksNameshasPksValuesで変更があった行のプライマリーキーのカラム名と値を確認
  • hasModifiedColumnsで変更されたカラムを確認
  • rowAtStartPointChangesの開始時点(Changes#setStartPointNow)でのカラムの値を確認
  • rowAtEndPointChangesの終了時点(Changes#setEndPointNow)で行がなくなっていることを確認

今回は扱っていませんが、ofCreationofCreationOnTableといったものを使って変更の種類や指定したテーブルの変更の種類に沿った
ナビゲーションもできるようです。

AssertJ - fluent assertions java library / AssertJ DB / Features highlight / Navigation / With Changes as root

最初、ナビゲーションの考え方を押さえていなくてTableRequestと同じように扱おうとしてかなりてこずりました…。

Changesを取得する対象は、特定のTableRequestに絞ることもできます。

    @Test
    void changesInclusion() {
        CategoryDao categoryDao = new CategoryDaoImpl(DomaConfig.singleton());
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Category javaCategory = new Category(1, "Java");

                    List<Book> javaBooks = List.of(
                            new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), javaCategory.id()),
                            new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), javaCategory.id()),
                            new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), javaCategory.id()),
                            new Book("978-4798151120", "独習Java 新版", 3278, LocalDate.of(2019, 5, 15), javaCategory.id())
                    );

                    categoryDao.insert(javaCategory);
                    javaBooks.forEach(bookDao::insert);

                    AssertDbConnection assertDbConnection = AssertDbConnectionFactory.of(DomaConfig.singleton().getDataSource()).create();

                    Table book = assertDbConnection.table("book").build();
                    Changes changes = assertDbConnection.changes().tables(book).build();
                    // 開始
                    changes.setStartPointNow();

                    Category mysqlCategory = new Category(2, "MySQL");

                    List<Book> mysqlBooks = List.of(
                            new Book("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, LocalDate.of(2024, 5, 22), mysqlCategory.id()),
                            new Book("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), mysqlCategory.id())
                    );

                    // insert
                    categoryDao.insert(mysqlCategory);
                    mysqlBooks.forEach(bookDao::insert);
                    // update(title、priceのみ)
                    bookDao.update(new Book("978-4621303252", "Effective Java", 3000, LocalDate.of(2018, 10, 30), javaCategory.id()));
                    bookDao.update(new Book("978-4774189093", "Java本格入門", 2500, LocalDate.of(2017, 4, 18), javaCategory.id()));
                    // delete
                    bookDao.delete(new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3), javaCategory.id()));

                    // 終了
                    changes.setEndPointNow();

                    assertThat(changes)
                            // 全体の変更数
                            .hasNumberOfChanges(5)
                            // 種類ごとの変更数
                            .ofCreation().hasNumberOfChanges(2)
                            .ofModification().hasNumberOfChanges(2)
                            .ofDeletion().hasNumberOfChanges(1)
                            // categoryテーブルの変更数
                            .onTable("category")
                            .hasNumberOfChanges(0)
                            // bookテーブルの変更数
                            .onTable("book")
                            .hasNumberOfChanges(5);

                    assertThat(changes)
                            // ひとつ目の変更(insert)
                            .change()
                            .isOnTable("book")
                            .isCreation()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4297141844")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .doesNotExist()
                            // 終了時点の行
                            .rowAtEndPoint()
                            .hasValues("978-4297141844", "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ", 3080, DateValue.from(LocalDate.of(2024, 5, 22)), 2)
                            // 2つ目の変更(insert)
                            .change()
                            .isOnTable("book")
                            .isCreation()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4798161488")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .doesNotExist()
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("isbn").isEqualTo("978-4798161488")
                            .value("title").isEqualTo("MySQL徹底入門 第4版 MySQL 8.0対応")
                            .value("price").isEqualTo(4180)
                            .value("publish_date").isEqualTo(DateValue.from(LocalDate.of(2020, 7, 6)))
                            .value("category_id").isEqualTo(2)
                            // 3つ目の変更(insert)は記録されない
                            // 4つ目の変更(update)
                            .change()
                            .isOnTable("book")
                            .isModification()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4621303252")
                            // 変更されたカラム
                            .hasModifiedColumns("title", "price")
                            // 変更前後のカラムの値を参照
                            .column("title").hasValues("Effective Java 第3版", "Effective Java")
                            .column("price").hasValues(4400, 3000)
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("title").isEqualTo("Effective Java 第3版")
                            .value("price").isEqualTo(4400)
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("title").isEqualTo("Effective Java")
                            .value("price").isEqualTo(3000)
                            // 5つ目の変更(update)
                            .change()
                            .isOnTable("book")
                            .isModification()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4774189093")
                            .hasModifiedColumns("title", "price")
                            // 変更前後のカラムの値を参照
                            .column("title").hasValues("Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", "Java本格入門")
                            .column("price").hasValues(3278, 2500)
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("title").isEqualTo("Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで")
                            .value("price").isEqualTo(3278)
                            // 終了時点の行
                            .rowAtEndPoint()
                            .value("title").isEqualTo("Java本格入門")
                            .value("price").isEqualTo(2500)
                            // 6つ目の変更(delete)
                            .change()
                            .isOnTable("book")
                            .isDeletion()
                            .hasPksNames("isbn")
                            .hasPksValues("978-4297144357")
                            // 開始時点の行
                            .rowAtStartPoint()
                            .value("isbn").isEqualTo("978-4297144357")
                            .value("title").isEqualTo("Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~")
                            // 終了時点の行
                            .rowAtEndPoint()
                            .doesNotExist();
                });
    }

こういった感じですね。

                    Table book = assertDbConnection.table("book").build();
                    Changes changes = assertDbConnection.changes().tables(book).build();

AssertJ - fluent assertions java library / AssertJ DB / Concepts / Elements of the database / Changes

今回はbookテーブルのみを対象にしているので、categoryテーブルの変更内容は検出されません。

                            // 3つ目の変更(insert)は記録されない

こんなところでしょうか。

オマケ:Changesでの変更内容の検出方法

Changesがどうやって変更内容を検出しているのか気になったので、ちょっと見てみました。

結論としては、Changes#setStartPointNowの時点でデータを取得しておき、Changes#setStartPointNowを呼び出した時点でのデータも
取得してその差分を算出しているようです。

Changes#setStartPointNowでデータを取得している箇所。

https://github.com/assertj/assertj-db/blob/assertj-db-3.0.0/src/main/java/org/assertj/db/type/Changes.java#L296-L308

Changes#setEndPointNowでデータを取得している箇所。

https://github.com/assertj/assertj-db/blob/assertj-db-3.0.0/src/main/java/org/assertj/db/type/Changes.java#L327-L337

実際の差分抽出は、このあたりで行っています。

https://github.com/assertj/assertj-db/blob/assertj-db-3.0.0/src/main/java/org/assertj/db/type/Changes.java#L351-L384

つまり、この仕組みを考えるとChangesは変更内容がAssertJ-DBから見える状態でないと機能しないことになります。

以下の状態で使うことになりますね。

実際にはテスト対象と同じトランザクション内で使うことが多いのかなと思います。

おわりに

データベースのデータに対するアサーションができるAssertJ-DBを試してみました。

TableRequestはすんなりいったものの、Changesはかなりハマりました…。ちゃんとドキュメントを読み込むべきでした…。

少し癖のある感じはしますが、Changesの変更検出はよいですね。慣れは必要ですが便利そうです。