概要

テストコードは書けるのに、後から読み返すと「何をテストしているのか分からない」「データの準備が長くてテスト本体が埋もれている」と感じたことはないでしょうか。プロダクションコードに設計パターンがあるように、テストコードにも可読性と保守性を高めるための定石があります。この記事では、テスト構造を明確にする Given-When-Then パターン、テストデータの組み立てを宣言的にする TestDataBuilder パターン、定形データの生成を一箇所に集約する Object Mother パターンの 3 つを中心に整理します。注文処理を題材に、テストフィクスチャの設計方針や、フィールドが増えたときにテスト側の修正を最小限にするための工夫まで示します。

使いどころ

注文・請求など多フィールドのドメインオブジェクトのテストで、データ準備コードの重複を TestDataBuilder で解消する

レビュー指摘が多いテストコードの可読性を Given-When-Then に揃えて改善する

マスタデータが多い統合テストで Object Mother パターンを導入してフィクスチャ管理を一元化する

入力条件だけを変えたい複数テストで、Builder のデフォルト値を使って差分だけ記述する

テストフィクスチャが成長してきたプロジェクトで、共通化と局所性のバランスを見直す

コード例

OrderServiceTest.java
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {

    record OrderItem(String productName, int quantity, BigDecimal unitPrice) {}
    record Order(String customerName, LocalDate orderDate, List<OrderItem> items, Order.Status status) {
        enum Status { PENDING, CONFIRMED, SHIPPED, CANCELLED }
        BigDecimal totalAmount() {
            return items.stream().map(i -> i.unitPrice().multiply(BigDecimal.valueOf(i.quantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        }
    }

    // --- TestDataBuilder パターン ---
    static class OrderBuilder {
        private String customerName = "テスト株式会社";
        private LocalDate orderDate = LocalDate.of(2025, 4, 1);
        private List<OrderItem> items = List.of(new OrderItem("コピー用紙", 10, new BigDecimal("500")));
        private Order.Status status = Order.Status.PENDING;
        OrderBuilder customerName(String v) { this.customerName = v; return this; }
        OrderBuilder items(List<OrderItem> v) { this.items = v; return this; }
        OrderBuilder status(Order.Status v) { this.status = v; return this; }
        Order build() { return new Order(customerName, orderDate, items, status); }
    }

    // --- Object Mother パターン ---
    static class OrderMother {
        static Order pendingOrder() { return new OrderBuilder().build(); }
        static Order confirmedOrder() { return new OrderBuilder().status(Order.Status.CONFIRMED).build(); }
        static Order highValueOrder() {
            return new OrderBuilder().customerName("大口商事")
                .items(List.of(new OrderItem("サーバー", 2, new BigDecimal("500000")))).build();
        }
    }

    static class OrderService {
        Order confirm(Order order) {
            if (order.status() != Order.Status.PENDING)
                throw new IllegalStateException("確定できるのは PENDING 状態の注文のみです");
            return new Order(order.customerName(), order.orderDate(), order.items(), Order.Status.CONFIRMED);
        }
        boolean requiresApproval(Order order) {
            return order.totalAmount().compareTo(new BigDecimal("100000")) > 0;
        }
    }

    private OrderService service;
    @BeforeEach void setUp() { service = new OrderService(); }

    @Nested @DisplayName("注文確定")
    class ConfirmTests {
        @Test @DisplayName("PENDING を確定すると CONFIRMED になる")
        void confirmPending() {
            Order confirmed = service.confirm(OrderMother.pendingOrder());
            assertEquals(Order.Status.CONFIRMED, confirmed.status());
        }
        @Test @DisplayName("CONFIRMED 済みの再確定は例外")
        void confirmAgainThrows() {
            assertThrows(IllegalStateException.class, () -> service.confirm(OrderMother.confirmedOrder()));
        }
    }

    @Nested @DisplayName("承認要否")
    class ApprovalTests {
        @Test @DisplayName("高額注文は承認が必要")
        void highValue() { assertTrue(service.requiresApproval(OrderMother.highValueOrder())); }
        @Test @DisplayName("少額注文は承認不要")
        void lowValue() { assertFalse(service.requiresApproval(OrderMother.pendingOrder())); }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でテストデータを不変に定義し、Builder が record を返す設計が自然に書ける。

Java 17
// Java 17: record + Builder で簡潔に
record Order(String customerName, int amount, Order.Status status) {
    enum Status { PENDING, CONFIRMED, SHIPPED }
}
Order order = new OrderBuilder()
    .customerName("山田商事").amount(50_000).build();

Library Comparison

手書きの TestDataBuilder外部依存なしで自由度が高く、プロジェクト固有のデフォルト値を埋め込める。Builder クラス自体の保守が必要。
Instancioテストデータをランダム生成し、必要なフィールドだけ上書きする。大量フィールドに効果的。失敗時の再現にシード指定が必要。業務ルールに沿った値は手動で上書きする。
EasyRandomPOJO のフィールドを再帰的にランダム埋めしたいとき。メンテナンス状況が不安定。Instancio の方が推奨される傾向。

注意点

Given-When-Then はコメントで区切るだけでも効果がある。形式にこだわりすぎて本質が見えにくくなっては本末転倒

TestDataBuilder のメソッドチェーンが深すぎると追いにくい。重要なフィールドだけ明示し、残りはデフォルトに任せる

Object Mother は変更頻度の高いフィールドが多い場合に withXxx が増殖しがち。Builder の方が合う場面もある

テストヘルパーの共通化範囲は「同一テストクラス内」または「同一パッケージ内」に留めるのが安全

Builder に業務ルールを入れすぎると、何がテスト対象で何が補助コードか分かりにくくなる。補助は最小限に保つ

Object Mother が巨大化すると副作用のある共通データ置き場になりやすい。用途別に小さく分割する方が扱いやすい

FAQ

Given-When-Then と Arrange-Act-Assert の違いは何ですか。

本質的に同じ構造で、BDD 寄りが Given-When-Then、xUnit 寄りが Arrange-Act-Assert です。

TestDataBuilder と Object Mother はどう使い分けますか。

フィールドの組み合わせが多い場合は Builder、定形パターンが数種類で済む場合は Object Mother が向いています。

テストデータのデフォルト値はどこに定義すべきですか。

Builder のフィールド初期値として定義するのが一般的です。テストで変更したい部分だけ上書きします。

Builder をどこまで共通化すべきですか。

同じドメインオブジェクトを複数テストクラスで繰り返し組み立てるなら共通化する価値があります。1クラス内だけでしか使わないなら、そのテストに閉じていた方が読みやすいこともあります。

テストコードにもリファクタリングは必要ですか。

必要です。重複や読みにくさを放置すると、仕様変更時にテスト修正コストが増えます。プロダクションコードと同様に、小さく継続的に整えるのが有効です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます