CLOVER🍀

That was when it all began.

Object MotherずTest Data Builder、Test Data BuilderずInstancioでテストデヌタ䜜成

これは、なにをしたくお曞いたもの

テストを曞く時に、テストデヌタの扱いには頭を悩たせたす。

ここでいう「テストデヌタ」ずは、デヌタストア、䞻にデヌタベヌスを察象にしたテストで䜿うデヌタのこずです。

どう䜜ったらいいのか、どうメンテナンスしおいったらいいのかだったり悩みは尜きないのですが、
テストデヌタに関するパタヌンがあるようなのでメモしおおきたす。

テストデヌタの悩み

ひずたず、察象の蚀語はJavaずしたしょう。

デヌタベヌスを䜿ったテストに぀いお調べるず、いろいろ出おきたす。

ずたあ、簡単にテストができる仕組みだったり方法はすぐに芋぀かりたす。こういった䞭から方針に合ったものを
遞ぶ、ずいう感じですね。

ただ、実際にやっおみるず悩むのはテストデヌタの扱いだったりしたす。具䜓的にはこういう悩みがあるかなず。

あくたで䞻芳です。

テストデヌタを䜜るのが倧倉

たず、テストデヌタを䜜るのが倧倉です。

䞖の䞭のサンプルコヌドだずさらっず枈たしおいるこずが倚いですが、実際のアプリケヌションを䜜る時には
倚くのテヌブルのデヌタが必芁になるこずが倚いでしょう。

倖郚キヌを䜿っおいるこずも倚いず思いたすので、そうなるず末端の機胜ほど関連するテヌブルが増えおいき
芋づる匏に䜜らないずいけないデヌタが倚くなりたす。

こうなるずたずどのようなデヌタを甚意する必芁があるのか、考えるのが倧倉です。

そしおテストケヌスごずに少しだけ内容が違うデヌタができたりしたす。

テストコヌドの䞭でテストデヌタのセットアップをするこずも倚いず思いたすが、その時にテストコヌド
そのものよりもテストデヌタをセットアップするコヌドの方が長くなったりしたす。

ではSQLやYAMLなどで甚意すればいいかずいうず、それは曞く堎所が違うだけずいう気がしたすね。

手段の話でいくず、このブログではDatabase Riderを䜿ったりしたした。

Database Riderを試してみる - CLOVER🍀

Spring Test × Database Riderで、データを作成する時にテーブル間の依存関係を記録する - CLOVER🍀

テストデヌタを共通化するず倉曎時の圱響が倧きいが、共通化しないず䌌たようなものがたくさん増えおいく

テストデヌタを䜜るのが倧倉だず、その手間を枛らすために共通化したくなるものです。
テストデヌタをコヌドで䜜っおいる堎合は特にそうかなず思いたす。

ただ、共通化しお䜜っおいるず倉曎時の圱響範囲がずおも倧きくなりたす。

でも個々のテストで同じものを䜜っおいるず、仕様が倉わったりしお倉曎する時にやっぱり修正しないず
いけない察象が個々に発生したす。

テストデヌタが読みにくい

テストデヌタは意倖ず読みにくい気がしたす。たずえコヌドで曞かれおいおも。

テストコヌドを倉曎する時に、その前提ずなるテストデヌタや期埅倀ずなるデヌタが読み解けず苊劎するこずが
倚い印象があるのですが、どうでしょうか。

アプリケヌションに曞かれる凊理ず違っお、あたり意志が芋えにくかったり、頭の䞭で1床テヌブルでの
持ち方に倉換しお考えようずするので思考のコストが倧きいからな気もするのですが。

耇合

ここたでがテストデヌタに察する悩みなのですが、この悩みそれぞれが絡み合っお話が難しくなっおいる気が
するんですよね。

テストデヌタを䜜るのが倧倉、しかも埌で芋返しおも読みにくい読めない。共通化しおいるので
倉曎するのも怖い。ならコピヌしおちょっずだけ違うものを䜜っお みたいなこずをするような気がしたす。

これ、みなさんどうやっお乗り越えおいっおるんでしょうね。

パタヌン

それはそうず、テストデヌタを䜜るパタヌンがあるようなのでちょっずメモしおおきたしょう。

  • Object Mother
  • Test Data Builder
Object Mother

Object MotherはMartin Fowlerが解説しおいるパタヌンで、Thoughtworksのプロゞェクトで考案されたものです。
テストで䜿甚するデヌタオブゞェクトを䜜るためのパタヌンです。

Object Mother

Object Mother

テストで組み立おる以䞋のようなコヌドを

Invoice invoice = new Invoice(
    new Recipient("Sherlock Holmes",
        new Address("221b Baker Street", 
                    "London", 
                    new PostCode("NW1", "3RX"))),
    new InvoiceLines(
        new InvoiceLine("Deerstalker Hat", 
            new PoundsShillingsPence(0, 3, 10)),
        new InvoiceLine("Tweed Cape", 
            new PoundsShillingsPence(0, 4, 12))));

ファクトリヌメ゜ッドにたずめたす。このファクトリヌメ゜ッドが定矩されたクラスがObject Motherです。

Invoice invoice = TestInvoices.newDeerstalkerAndCapeInvoice();

Object Motherを䜿うこずで、テストデヌタオブゞェクトを生成するコヌドをテストコヌドから分離、
ファクトリヌメ゜ッドに適切なメ゜ッドを぀けるこずで可読性も向䞊したす。テスト間での再利甚も可胜です。

なのですが、Object Motherはテストデヌタのバリ゚ヌションに察応できたせん。少し異なるテストデヌタが
必芁になるず、Object Motherに別のファクトリヌメ゜ッドが远加されたす。

Invoice invoice1 = TestInvoices.newDeerstalkerAndCapeAndSwordstickInvoice();
Invoice invoice2 = TestInvoices.newDeerstalkerAndBootsInvoice();

こちらのペヌゞには、Object Motherはアンチパタヌンなのではず曞かれおいたす。

Object Mother

このような結果、Object Motherがすべおのものを飲み蟌んだGod ClassGod Object Motherに
なりやすいからです。結果ずしおメンテナンスが困難になっおいきたす。

Object Motherでは固定のデヌタになっおしたうため、Builderパタヌンを適甚した方がよいのではないか
ずいうこずで出おきたのがTest Data Builderです。

Test Data Builder

Test Data BuilderはNat Pryceが「実践テスト駆動開発」の䞭で、Object Motherの匱点を述べたうえで
玹介しおいるパタヌンです。

Mistaeks I Hav Made: Test Data Builders: an alternative to the Object Mother pattern

Test Data Builder

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる(和智 右桂 和智 右桂 髙木 正弘 髙木 正弘 Steve Freeman Nat Pryce)|翔泳社の本

ちなみに、Object Motherの玹介で曞いたサンプルコヌドはTest Data Builderの゚ントリヌから持っおきたものです。
実践テスト駆動開発でもよく䌌たコヌドが曞かれおいたす。

以䞋はTest Data Builderの䟋です。

public class InvoiceBuilder {
    Recipient recipient = new RecipientBuilder().build();
    InvoiceLines lines = new InvoiceLines(new InvoiceLineBuilder().build());
    PoundsShillingsPence discount = PoundsShillingsPence.ZERO;

    public InvoiceBuilder withRecipient(Recipient recipient) {
        this.recipient = recipient;
        return this;
    }

    public InvoiceBuilder withInvoiceLines(InvoiceLines lines) {
        this.lines = lines;
        return this;
    }

    public InvoiceBuilder withDiscount(PoundsShillingsPence discount) {
        this.discount = discount;
        return this;
    }

    public Invoice build() {
        return new Invoice(recipient, lines, discount);
    }
}

ポむントは以䞋になりたす。

デフォルト倀でよければ、以䞋の呌び出しで察象のむンスタンスが手に入りたす。

Invoice anInvoice = new InvoiceBuilder().build();

Builderを䜿うこずで、むンスタンスの倀をカスタマむズできたす。

Invoice invoiceWithNoPostcode = new InvoiceBuilder()
    .withRecipient(new RecipientBuilder()
        .withAddress(new AddressBuilder()
            .withNoPostcode()
            .build())
        .build())
    .build();


new AddressBuilder()
    .withName("Sherlock Holmes")
    .withStreet("221b Baker Street")
    .withCity("London")
    .withPostCode("NW1", "3RX")
    .build();

たたBuilderに別のBuilderを枡しおむンスタンスをカスタマむズできるようにしおもよいでしょう。

で、どうなの

で、Test Data Builderを䜿えば自分が思っおいる悩みポむントはすっきり解決するかずいうず、意倖ずそうでも
ありたせん。

このあたりが理由ですね。

  • Builderを䜜るのが倧倉
  • Builderでカスタマむズされたテストデヌタを読み解くのが倧倉
  • Builderで䜜られたGod Object Motherができあがる可胜性が高い

たあ、うたい方法はないずいう気はしたす。

そもそも

ずいう感じですからね。

Database RiderやAseertJ-DBの曎新が滞っおいるのを芋るず、なかなか厳しいんだろうなず。

割ずJava寄りの話にしおいたすが、他の蚀語っおDbUnitのような立ち䜍眮のラむブラリヌっおあんたりない気が
したす。

Test Data Builder通垞のアサヌション、ずいうのが萜ち着き先なのかもしれたせん。

最埌に軜くInstancioを䜿っおTest Data Builderを詊しおみたしょう。Instancioはテストデヌタをランダムに
生成するラむブラリヌです。

テストデータをランダムに生成してインスタンスに設定するライブラリー、Instancioを試す - CLOVER🍀

環境

今回の環境はこちら。

$ java --version
openjdk 25.0.2 2026-01-20
OpenJDK Runtime Environment (build 25.0.2+10-Ubuntu-124.04)
OpenJDK 64-Bit Server VM (build 25.0.2+10-Ubuntu-124.04, mixed mode, sharing)


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

デヌタベヌスにはMySQLを䜿いたす。

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

MySQLは172.17.0.2で動䜜しおいるものずしたす。

準備

Maven䟝存関係などはこちら。

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

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

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>6.0.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.27.7</version>
        </dependency>

        <dependency>
            <groupId>org.instancio</groupId>
            <artifactId>instancio-core</artifactId>
            <version>5.5.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

デヌタベヌスアクセスラむブラリヌずしおはDomaを䜿いたす。

お題

今回のお題は、ブログずその著者にしたしょう。

こういうテヌブルにしたす。

create table if not exists author(
  id varchar(36),
  first_name varchar(10),
  last_name varchar(10),
  age integer,
  primary key(id)
);

create table if not exists author(
  id varchar(36),
  first_name varchar(10),
  last_name varchar(10),
  age integer,
  primary key(id)
);

゜ヌスコヌドを䜜成する

このお題で゜ヌスコヌドを䜜成しおおきたす。

Domaの゚ンティティヌクラス。

src/main/java/org/littlewings/testdatabuilder/entity/Author.java

package org.littlewings.testdatabuilder.entity;

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

@Entity
public record Author(
        @Id
        String id,
        String firstName,
        String lastName,
        Integer age
) {
}

src/main/java/org/littlewings/testdatabuilder/entity/Post.java

package org.littlewings.testdatabuilder.entity;

import java.time.LocalDate;
import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public record Post(
        @Id
        String id,
        String title,
        String url,
        LocalDate date,
        String authorId
) {
}

Dao。

src/main/java/org/littlewings/testdatabuilder/dao/AuthorDao.java

package org.littlewings.testdatabuilder.dao;

import java.util.List;
import org.littlewings.testdatabuilder.entity.Author;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Script;
import org.seasar.doma.Select;
import org.seasar.doma.Sql;
import org.seasar.doma.jdbc.Result;

@Dao
public interface AuthorDao {
    @Sql("""
            create table if not exists author(
              id varchar(36),
              first_name varchar(10),
              last_name varchar(10),
              age integer,
              primary key(id)
            )
            """)
    @Script
    void createTableIfExistsRecreate();

    @Sql("delete from author")
    @Delete
    int deleteAll();

    @Insert
    Result<Author> insert(Author author);

    @Sql("select /*%expand*/* from author where id = /* id */'dummy'")
    @Select
    Author selectById(String id);

    @Sql("select /*%expand*/* from author order by age desc")
    @Select
    List<Author> selectAllOrderByAgeDesc();
}

src/main/java/org/littlewings/testdatabuilder/dao/PostDao.java

package org.littlewings.testdatabuilder.dao;

import java.util.List;
import org.littlewings.testdatabuilder.entity.Post;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.In;
import org.seasar.doma.Insert;
import org.seasar.doma.Script;
import org.seasar.doma.Select;
import org.seasar.doma.Sql;
import org.seasar.doma.jdbc.Result;

@Dao
public interface PostDao {
    @Sql("""
            create table if not exists post(
              id varchar(36),
              title varchar(200),
              url text,
              date date,
              author_id varchar(36),
              primary key(id),
              foreign key(author_id) references author(id)
            )
            """)
    @Script
    void createTableIfExistsRecreate();

    @Sql("delete from post")
    @Delete
    int deleteAll();

    @Insert
    Result<Post> insert(Post post);

    @Sql("select /*%expand*/* from post order by date desc")
    @Select
    List<Post> selectAllOrderByDateDesc();
}

Domaの蚭定クラス。

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

package org.littlewings.testdatabuilder;

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 INSTANCE = new DomaConfig();

    private Dialect dialect;
    private LocalTransactionDataSource dataSource;
    private TransactionManager transactionManager;

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

    public static DomaConfig singleton() {
        return INSTANCE;
    }

    @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;
    }
}

テストコヌドを曞く

では、テストコヌドを曞いおいきたす。

テストコヌドの雛圢はこちら。

src/test/java/org/littlewings/testdatabuilder/TestDataBuilderTest.java

package org.littlewings.testdatabuilder;

import java.net.URL;
import java.time.LocalDate;
import java.util.List;
import org.instancio.Instancio;
import org.instancio.Select;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.littlewings.testdatabuilder.dao.AuthorDao;
import org.littlewings.testdatabuilder.dao.AuthorDaoImpl;
import org.littlewings.testdatabuilder.dao.PostDao;
import org.littlewings.testdatabuilder.dao.PostDaoImpl;
import org.littlewings.testdatabuilder.entity.Author;
import org.littlewings.testdatabuilder.entity.Post;

import static org.assertj.core.api.Assertions.assertThat;

class TestDataBuilderTest {
    @BeforeAll
    static void setUpAll() {
        AuthorDao authorDao = new AuthorDaoImpl(DomaConfig.singleton());
        authorDao.createTableIfExistsRecreate();

        PostDao postDao = new PostDaoImpl(DomaConfig.singleton());
        postDao.createTableIfExistsRecreate();
    }

    @BeforeEach
    void setUp() {
        PostDao postDao = new PostDaoImpl(DomaConfig.singleton());
        postDao.deleteAll();

        AuthorDao authorDao = new AuthorDaoImpl(DomaConfig.singleton());
        authorDao.deleteAll();
    }

    // ここにテストを曞く
}
Instancioをそのたた䜿う

たずはシンプルにInstancioを䜿っおテストを曞いおみたす。

こんな感じでしょうか。

    @Test
    void authors() {
        Author katsuo = Instancio.of(Author.class)
                .set(Select.field("firstName"), "カツオ")
                .set(Select.field("age"), 11)
                .create();
        Author wakame = Instancio.of(Author.class)
                .set(Select.field("firstName"), "ワカメ")
                .set(Select.field("age"), 9)
                .create();

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    AuthorDao authorDao = new AuthorDaoImpl(DomaConfig.singleton());
                    authorDao.insert(katsuo);
                    authorDao.insert(wakame);

                    Author foundKatsuo = authorDao.selectById(katsuo.id());
                    assertThat(foundKatsuo).isEqualTo(katsuo);

                    Author foundWakame = authorDao.selectById(wakame.id());
                    assertThat(foundWakame).isEqualTo(wakame);
                });
    }

ある意味これもTest Data Builderず蚀える気はしたすが。

Instancioを䜿っおTest Data Builderを䜜成する

次はInstancioを䜿っおTest Data Builderを䜜りたす。

こんな感じでしょうか。

src/test/java/org/littlewings/testdatabuilder/AuthorFactory.java

package org.littlewings.testdatabuilder;

import java.util.function.Consumer;
import org.instancio.Instancio;
import org.instancio.InstancioApi;
import org.instancio.Select;
import org.littlewings.testdatabuilder.dao.AuthorDao;
import org.littlewings.testdatabuilder.entity.Author;

public class AuthorFactory {
    private InstancioApi<Author> authorCreator;
    private AuthorDao authorDao;

    private AuthorFactory(AuthorDao authorDao) {
        this.authorDao = authorDao;
        this.authorCreator = Instancio.of(Author.class)
                .generate(Select.field("id"), gen -> gen.string().minLength(36).maxLength(36))
                .generate(Select.field("firstName"), gen -> gen.string().maxLength(10))
                .generate(Select.field("lastName"), gen -> gen.string().maxLength(10))
                .generate(Select.field("age"), gen -> gen.ints().min(0).max(100));
    }

    public static AuthorFactory anAuthor(AuthorDao authorDao) {
        return new AuthorFactory(authorDao);
    }

    public AuthorFactory firstName(String firstName) {
        authorCreator.set(Select.field("firstName"), firstName);
        return this;
    }

    public AuthorFactory age(int age) {
        authorCreator.set(Select.field("age"), age);
        return this;
    }

    public AuthorFactory with(Consumer<InstancioApi<Author>> consumer) {
        consumer.accept(authorCreator);
        return this;
    }

    public Author build() {
        return authorCreator.create();
    }

    public Author create() {
        Author instance = build();
        authorDao.insert(instance);

        return instance;
    }
}

src/test/java/org/littlewings/testdatabuilder/PostFactory.java

package org.littlewings.testdatabuilder;

import java.net.URL;
import java.time.LocalDate;
import java.util.function.Consumer;
import org.instancio.Instancio;
import org.instancio.InstancioApi;
import org.instancio.Select;
import org.littlewings.testdatabuilder.dao.PostDao;
import org.littlewings.testdatabuilder.entity.Author;
import org.littlewings.testdatabuilder.entity.Post;

public class PostFactory {
    private InstancioApi<Post> postCreator;
    private PostDao postDao;

    private PostFactory(PostDao postDao) {
        this.postDao = postDao;
        this.postCreator = Instancio.of(Post.class)
                .generate(Select.field("id"), gen -> gen.string().minLength(36).maxLength(36))
                .generate(Select.field("title"), gen -> gen.string().maxLength(200))
                .generate(Select.field("url"), gen -> gen.net().url().protocol("https").as(URL::toString));
    }

    public static PostFactory aPost(PostDao postDao) {
        return new PostFactory(postDao);
    }

    public static PostFactory aPost(PostDao postDao, Author author) {
        return aPost(postDao).withAuthor(author);
    }


    public PostFactory date(LocalDate date) {
        postCreator.set(Select.field("date"), date);
        return this;
    }

    public PostFactory with(Consumer<InstancioApi<Post>> consumer) {
        consumer.accept(postCreator);
        return this;
    }

    public PostFactory withAuthor(Author author) {
        postCreator.set(Select.field("authorId"), author.id());
        return this;
    }

    public Post build() {
        return postCreator.create();
    }

    public Post create() {
        Post instance = build();
        postDao.insert(instance);

        return instance;
    }
}

生成するむンスタンスの倀は少々カスタマむズしおありたす。

    private AuthorFactory(AuthorDao authorDao) {
        this.authorDao = authorDao;
        this.authorCreator = Instancio.of(Author.class)
                .generate(Select.field("id"), gen -> gen.string().minLength(36).maxLength(36))
                .generate(Select.field("firstName"), gen -> gen.string().maxLength(10))
                .generate(Select.field("lastName"), gen -> gen.string().maxLength(10))
                .generate(Select.field("age"), gen -> gen.ints().min(0).max(100));
    }


    private PostFactory(PostDao postDao) {
        this.postDao = postDao;
        this.postCreator = Instancio.of(Post.class)
                .generate(Select.field("id"), gen -> gen.string().minLength(36).maxLength(36))
                .generate(Select.field("title"), gen -> gen.string().maxLength(200))
                .generate(Select.field("url"), gen -> gen.net().url().protocol("https").as(URL::toString));
    }

デヌタベヌスに保存するタむミングをちょっず悩みたしたが、オブゞェクトの構築時にinsertするこずにしたした。

    public Author build() {
        return authorCreator.create();
    }

    public Author create() {
        Author instance = build();
        authorDao.insert(instance);

        return instance;
    }

insertせずにむンスタンス化たででずどめおおくメ゜ッドも甚意しおいたす。

こういう圹割もあった関係で、Test Data Builderずいうパタヌンの割にクラス名をBuilderにせずにFactoryにしたした。

䜿うずこんな感じですね。

    @Test
    void authorsWithBuilder() {
        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    AuthorDao authorDao = new AuthorDaoImpl(DomaConfig.singleton());

                    Author katsuo = AuthorFactory.anAuthor(authorDao)
                            .firstName("カツオ")
                            .age(11)
                            .create();
                    Author wakame = AuthorFactory.anAuthor(authorDao)
                            .firstName("ワカメ")
                            .age(9)
                            .create();

                    Author foundKatsuo = authorDao.selectById(katsuo.id());
                    assertThat(foundKatsuo).isEqualTo(katsuo);

                    Author foundWakame = authorDao.selectById(wakame.id());
                    assertThat(foundWakame).isEqualTo(wakame);
                });
    }

    @Test
    void postsWithBuilder() {
        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    AuthorDao authorDao = new AuthorDaoImpl(DomaConfig.singleton());
                    PostDao postDao = new PostDaoImpl(DomaConfig.singleton());

                    Author katsuo = AuthorFactory.anAuthor(authorDao)
                            .firstName("カツオ")
                            .age(11)
                            .create();
                    Author wakame = AuthorFactory.anAuthor(authorDao)
                            .firstName("ワカメ")
                            .age(9)
                            .create();

                    Post katsuoPost1 = PostFactory.aPost(postDao, katsuo)
                            .date(LocalDate.of(2026, 1, 31))
                            .create();

                    Post katsuoPost2 = PostFactory.aPost(postDao, katsuo)
                            .date(LocalDate.of(2026, 2, 5))
                            .create();

                    Post wakamePost1 = PostFactory.aPost(postDao, wakame)
                            .date(LocalDate.of(2026, 2, 1))
                            .create();

                    List<Post> posts = postDao.selectAllOrderByDateDesc();

                    assertThat(posts).hasSize(3);
                    assertThat(posts).isEqualTo(List.of(katsuoPost2, wakamePost1, katsuoPost1));
                });
    }

それから、Test Data Builderずはちょっず蚀い難いですが、たずめたものも䜜っおみたした。

src/test/java/org/littlewings/testdatabuilder/IsonoFamilyPostFactory.java

package org.littlewings.testdatabuilder;

import java.time.LocalDate;
import java.util.List;
import org.littlewings.testdatabuilder.dao.AuthorDao;
import org.littlewings.testdatabuilder.dao.PostDao;
import org.littlewings.testdatabuilder.entity.Author;
import org.littlewings.testdatabuilder.entity.Post;

public class IsonoFamilyPostFactory {
    private List<AuthorFactory> authorFactories;
    private List<PostFactory> postFactories;

    private IsonoFamilyPostFactory(AuthorDao authorDao, PostDao postDao) {
        this.authorFactories = List.of(
                AuthorFactory.anAuthor(authorDao)
                        .firstName("カツオ")
                        .age(11),
                AuthorFactory.anAuthor(authorDao)
                        .firstName("ワカメ")
                        .age(9)
        );

        this.postFactories = List.of(
                PostFactory.aPost(postDao)
                        .date(LocalDate.of(2026, 1, 31)),
                PostFactory.aPost(postDao)
                        .date(LocalDate.of(2026, 2, 5)),
                PostFactory.aPost(postDao)
                        .date(LocalDate.of(2026, 2, 1))
        );
    }

    public static IsonoFamilyPostFactory family(AuthorDao authorDao, PostDao postDao) {
        return new IsonoFamilyPostFactory(authorDao, postDao);
    }

    public List<Post> create() {
        List<Author> authors = authorFactories.stream().map(AuthorFactory::create).toList();

        postFactories.get(0).withAuthor(authors.get(0));
        postFactories.get(1).withAuthor(authors.get(0));
        postFactories.get(2).withAuthor(authors.get(1));

        return  postFactories.stream().map(PostFactory::create).toList();
    }
}

テスト偎。

    @Test
    void isonoFamiry() {
        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    AuthorDao authorDao = new AuthorDaoImpl(DomaConfig.singleton());
                    PostDao postDao = new PostDaoImpl(DomaConfig.singleton());

                    IsonoFamilyPostFactory isonoFamilyPostFactory = IsonoFamilyPostFactory.family(authorDao, postDao);
                    List<Post> isonoFamilyPosts = isonoFamilyPostFactory.create();

                    List<Post> posts = postDao.selectAllOrderByDateDesc();

                    assertThat(posts).hasSize(isonoFamilyPosts.size());
                    assertThat(posts).isEqualTo(List.of(isonoFamilyPosts.get(1), isonoFamilyPosts.get(2), isonoFamilyPosts.get(0)));
                });
    }

雰囲気を掎むために曞いおみたしたが、こんなずころでしょうか。

実際に曞いおみお

今回、Test Data BuilderずInstancioを組み合わせおみたしたが、たあ難しいですね。

最初に思ったように、こうなる感じがすごくしたした。

  • Builderを䜜るのが倧倉
  • Builderでカスタマむズされたテストデヌタを読み解くのが倧倉
  • Builderで䜜られたGod Object Motherができあがる可胜性が高い

Builderの蚭蚈が難しいのず、倉に凝るず自分しか䜿えないTest Data Builderができあがりそうだな、ず。

たたデヌタベヌスに保存するタむミングも厄介で、ここにデヌタ間の䟝存関係が入っおくるずTest Data Builderの蚭蚈が
かなり難しくなりたす。最埌に䜜ったテストデヌタをたずめたクラスでそこにチャレンゞしようかなず思ったのですが、
今回の゚ントリヌずしおは話がややこしくなりすぎるのでやめたした。

それから、これを汎甚的に䜜るのは厳しいでしょうね、ずいう印象も持ちたした。

ただ課題ずしおはいいなず思ったので、デヌタ間の䟝存関係も螏たえたうえでたたチャレンゞしたいですね。

おわりに

Object MotherずTest Data Builderに぀いお調べ぀぀、Test Data BuilderずInstancioを組み合わせおテストデヌタを䜜成しお
みたした。

単玔にBuilderを䜜るだけならいいのですが、実際にテストをこなせるデヌタを䜜る仕掛けを考えるずなるず䞀気に難易床が
跳ね䞊がる気がしたす。

最埌の方にも曞きたしたが、いい課題だずは思うのでたた考えおみたいです。