概要
テストコードは書けるのに、後から読み返すと「何をテストしているのか分からない」「データの準備が長くてテスト本体が埋もれている」と感じたことはないでしょうか。プロダクションコードに設計パターンがあるように、テストコードにも可読性と保守性を高めるための定石があります。この記事では、テスト構造を明確にする Given-When-Then パターン、テストデータの組み立てを宣言的にする TestDataBuilder パターン、定形データの生成を一箇所に集約する Object Mother パターンの 3 つを中心に整理します。注文処理を題材に、テストフィクスチャの設計方針や、フィールドが増えたときにテスト側の修正を最小限にするための工夫まで示します。
使いどころ
注文・請求など多フィールドのドメインオブジェクトのテストで、データ準備コードの重複を TestDataBuilder で解消する
レビュー指摘が多いテストコードの可読性を Given-When-Then に揃えて改善する
マスタデータが多い統合テストで Object Mother パターンを導入してフィクスチャ管理を一元化する
入力条件だけを変えたい複数テストで、Builder のデフォルト値を使って差分だけ記述する
テストフィクスチャが成長してきたプロジェクトで、共通化と局所性のバランスを見直す
コード例
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())); }
}
}Version Coverage
record でテストデータを不変に定義し、Builder が record を返す設計が自然に書ける。
// 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
注意点
Given-When-Then はコメントで区切るだけでも効果がある。形式にこだわりすぎて本質が見えにくくなっては本末転倒
TestDataBuilder のメソッドチェーンが深すぎると追いにくい。重要なフィールドだけ明示し、残りはデフォルトに任せる
Object Mother は変更頻度の高いフィールドが多い場合に withXxx が増殖しがち。Builder の方が合う場面もある
テストヘルパーの共通化範囲は「同一テストクラス内」または「同一パッケージ内」に留めるのが安全
Builder に業務ルールを入れすぎると、何がテスト対象で何が補助コードか分かりにくくなる。補助は最小限に保つ
Object Mother が巨大化すると副作用のある共通データ置き場になりやすい。用途別に小さく分割する方が扱いやすい
FAQ
本質的に同じ構造で、BDD 寄りが Given-When-Then、xUnit 寄りが Arrange-Act-Assert です。
フィールドの組み合わせが多い場合は Builder、定形パターンが数種類で済む場合は Object Mother が向いています。
Builder のフィールド初期値として定義するのが一般的です。テストで変更したい部分だけ上書きします。
同じドメインオブジェクトを複数テストクラスで繰り返し組み立てるなら共通化する価値があります。1クラス内だけでしか使わないなら、そのテストに閉じていた方が読みやすいこともあります。
必要です。重複や読みにくさを放置すると、仕様変更時にテスト修正コストが増えます。プロダクションコードと同様に、小さく継続的に整えるのが有効です。