テストから書くとドメインモデルはどう変わるか
テストを先に書くと、モデルの形が変わることがあります。この章はその範囲を扱います。
先に断っておくと、この章は「TDDテスト駆動開発 (TDD)実装より先にテストを書き、そのテストを通す形で実装を進める書き方。テストを書く時点が実装より前にあることが要件で、カバレッジを上げる活動とは別。 を使えば良いドメインモデルができる」とは主張しません。テスト先行が何を見えるようにし、何を見えるようにしないかを書きます。後者の方が実務では重要です。
DDD の戦術パターンそのものは扱いません。値オブジェクトをどう設計するか、集約の境界をどう引くかは、それぞれ専用の章があります。この章に出てくるコードはテストだけです。
この章で学ぶこと
- テストを先に書くと、モデルの何が見えるようになるか
- 見えたものを設計の根拠として扱うときの限度
- テストからは決まらないもの
03 テストが設計を圧す と 07 境界を差し替える を使います。DDD の用語を知らなくても読めますが、詳しく学ぶなら下の参照先へ進んでください。
この章で扱わないこと
この章は観察だけを書きます。設計そのものは、すべて既存のガイドが扱います。
| 観点 | 参照先 |
|---|---|
| 値オブジェクトの設計 (不変性・等価性・自己検証) | 値オブジェクト |
| エンティティと識別子 | エンティティ |
| 集約の境界と整合性 | 集約 |
| ドメインと永続化の境界 | リポジトリパターン |
| ドメインイベント | ドメインイベント |
| ユビキタス言語、境界づけられたコンテキスト | 戦略的設計 |
| 業務からモデルを見つける手法 | イベントストーミング |
| 原則とパターンの対応 | OOP と DDD の戦術パターン |
| 層ごとのテスト戦略、モックモック (Mock)期待する呼ばれ方を先に設定し、その通りに呼ばれたかを検証するテストダブル。検証を自分で行う点がスパイと違う。の配分 | テスト戦略 |
1. 依存がコンストラクタへ出てくる
07 章で見たとおりです。テストから対象を組み立てようとすると、内部で作っていたものを外から渡す形になります。
// テストが要求した形
$useCase = new ReserveRoom($clock, $repository, $notifier);
結果として、そのクラスが外の何に触るかがコンストラクタに並びます。
これは DDD の教義から導いたのではありません。テストを書くという制約が押し出しただけです。同じ形は、DDD と無関係なコードでも出ます。
ただし帰結は近いところに着地します。ドメインの中心にあるオブジェクトが、データベースや時計を直接呼ばない形になるからです。
2. 等価性を問われる
テストを書こうとすると、「これとこれが等しい」と書く場面が来ます。
test('同じ時間帯を表す 2 つは等しい', function () {
$a = new TimeRange(at('10:00'), at('11:00'));
$b = new TimeRange(at('10:00'), at('11:00'));
expect($a->equals($b))->toBeTrue();
});
equals() はまだありません。テストを書こうとして初めて、必要だと分かります。
このテストを書くには、何が等しければ等しいのかを決める必要があります。開始と終了が同じなら等しいのか。それとも、別々に作った予約は別のものなのか。
決めた結果が、そのオブジェクトの性質を決めます。値として扱うのか、同一性を持つものとして扱うのか。DDD ではこれを値オブジェクトとエンティティとして区別しますが、テストは区別を教えてくれません。問いを出すだけです。
答えるのは業務の側です。「同じ時間帯の 2 つの予約は同じ予約か」は、コードを見ても分かりません。
3. setup の痛みが境界を疑わせる
1 つのテストを書くのに、5 つも 6 つもオブジェクトを組み立てることがあります。次は 06 章からの続きではなく、組織や建物まで抱えた別の設計を仮に置いた例です。
test('予約を受け付ける', function () {
$room = new Room(new RoomId('room-1'), '会議室 A', 8);
$building = new Building(new BuildingId('b-1'), '本社', [$room]);
$organization = new Organization(new OrgId('o-1'), [$building]);
$policy = new ReservationPolicy($organization, maxHours: 4);
$calendar = new Calendar($organization, holidays: []);
// ここまで来てようやく本題
});
これは何かを言っています。ただし言っている内容は 1 つではありません。03 章の型 4 で触れたとおり、可能性は 2 つです。
- 対象が持ちすぎている。1 つの操作に必要な協力者が多すぎる
- テストの書き方が冗長なだけで、対象の設計には問題がない
見分ける手がかりは、組み立てたもののうち、そのテストの本題に関係するものがいくつあるかです。上の例で「予約を受け付ける」の本題に関わるのは、部屋と、上限時間くらいです。建物や組織は、部屋にたどり着くために作っているだけです。
この場合、対象が広い範囲を一度に要求していることになります。整合性を保つ単位を狭くできないかを疑う材料になります。
ここで止めます。「setup が長い、だから集約の境界が間違っている」とは言えません。テストデータビルダーのような道具で短くできるなら、それで済む話かもしれません。テストが出すのは疑う材料であって、結論ではありません。
4. テストからは決まらないもの
ここまでの 3 つは、テストが何かを見せてくれる話でした。同じ重みで、見せてくれないものを挙げます。
用語。Reservation と呼ぶか Booking と呼ぶか。この判断にテストは関与しません。業務の現場で使われている言葉を使うべきで、その言葉は現場でしか分かりません。
**境界の位置。**予約と請求を同じシステムで扱うのか、分けるのか。これも業務の側の判断です。テストはどちらの構成でも書けます。
**何を作るか。**そもそも会議室予約に何の機能が要るのか。テストは「作ると決めたもの」が正しく動くかを確かめますが、作るべきかどうかは扱いません。
**モデルの妥当性。**テストが全部緑でも、モデルが業務の実態からずれていることはあります。テストは自分で書いた期待に対して緑になるだけで、その期待が業務と合っているかは別の話です。
この 4 つは、DDD で言えば戦略的設計にあたる領域です。**テスト先行はここには届きません。**この章が「DDD は 1 章だけ」で足りると判断した理由がこれです。テストから見えるのは戦術の側だけで、それも問いの形でしか出てきません。
相性について
「TDD と DDD は相性が良い」という言い方があります。この章の観察から言えるのは、次の範囲です。
**言えること。**テスト先行で書くと、依存が外に出て、等価性を問われ、setup の重さが測れます。これらは DDD が扱う判断と重なる領域です。
**言えないこと。**TDD を使えば良いドメインモデルができる、とは言えません。テストが出すのは問いであって、答えは業務の側にあります。答えを間違えたまま緑にすることは容易です。
TDD を設計の手段として位置づける立場は、Freeman と Pryce の仕事に整理があります1。ドメインモデルを業務から見つける手法とは別の系統の話なので、両方を読むと補い合います。
まとめ
- テスト先行は、依存をコンストラクタへ押し出します。DDD の教義からではなく、テストの制約からそうなります
- 等価性の判断を問われますが、答えは業務の側にあります。テストは問いを出すだけです
- setup の重さは境界を疑う材料になります。材料であって結論ではありません
- 用語、境界の位置、何を作るか、モデルの妥当性は、テストからは決まりません
- 「相性が良い」と言えるのは戦術の側に限られ、それも問いの形で重なるという範囲です
次に読む
- 14 TDD が効かない場面 — 適用のコストが何に比例するか
- Laravel × DDD × クリーンアーキテクチャ実践ガイド — この章が送った先の全体。値オブジェクトから集約、リポジトリまでの設計を扱います