メインコンテンツまでスキップ

第58章:集約のテスト:不変条件が守れるか🧪🔒

この章はひとことで言うと、集約(Aggregate)が「仕様を守る装置」になってるかを、テストで証明する章だよ〜!💖 集約は「でっかいクラス」じゃなくて、**ルールを守るための“安全な箱”**だから、テストは超・主役です🏯🛡️


この章のゴール🎯✨

  • 集約テストで何を確認するべきか、迷わなくなる🧭
  • 不変条件(絶対守るルール)状態遷移を、テストでガチガチに固められる🔒
  • 似たテストを“量産”しないで、パラメータ化でキレイに書ける🧷
  • 「テストが仕様書になる」感覚をつかむ📘🧪

2026年2月時点の “テスト環境” の現実(超ざっくり)🧡🧰

  • TypeScript は 5.9 系が最新安定(5.9.2 が stable 扱い)だよ📌 (GitHub)
  • Node.js は v24 が Active LTS(安定して長く使うならここが無難)🟢 (Node.js)
  • テストは Vitest 4 系が安定枠(4.0.18 が latest、4.1 は beta が動いてる)🧪 (Yarn)
  • Jest は Jest 30 が stable(公式が “Current version (Stable)” って言ってる)📌 (Jest)

この章では **「集約テストの考え方」**が本体なので、例は Vitest で書くね(Jestでもほぼ同じノリでいけるよ)💕


集約テストって、何を守るの?🧠🔒

✅ 集約テストで守るものトップ3🥇🥈🥉

  1. 不変条件(Invariant) 例:
  • 支払い後は明細を変更できない
  • 明細が空の注文は確定できない
  • 合計金額は常に「明細の合計」と一致する
  1. 状態遷移(State Transition) 例:Draft → Confirmed → Paid → Fulfilled
  • 飛ばし禁止(Draft から Paid に直行しない)🚫
  • 逆戻り禁止(Paid → Confirmed に戻らない)🚫
  1. 集約内の整合性(Consistency) 例:
  • 明細を増減したら total が必ず更新される
  • 同じ商品を重複追加したときの扱い(統合する?別行にする?)がブレない

集約テストの基本方針(これだけ覚えて!)🧪💡

1) 「公開メソッドだけ」を叩く👆✨

集約の中の配列を直接いじったり、フィールドを書き換えたりは テストでも禁止🙅‍♀️ テストがズルすると、設計が崩れるよ〜💥

2) テスト名は “仕様の文章” にする📘✨

おすすめ:

  • it('明細が空の注文は確定できない')
  • it('支払い後は明細を追加できない')

これだけで、あとで読み返したとき神です🙏💕

3) 正常系より「禁止ルート」を厚めにする🚫🛡️

集約は “壊れないこと” が価値だから、 「できちゃダメ」をガッツリテストするのが強い💪✨


テストの準備:Fixture(テスト用の作りやすい注文)を作ろう🧁🏗️

集約テストは「状態」が大事だから、毎回ゼロから作ると疲れるの…😵‍💫 そこで Fixture(または Builder) を作ると超ラク💖

🍰 例:Order を作る Fixture イメージ

  • givenDraftOrder():下書き注文(明細あり/なしを選べる)
  • givenConfirmedOrder():確定済み注文
  • givenPaidOrder():支払い済み注文

これがあると、テストが 短く・読みやすく・壊れにくい


実例:Order 集約テスト(Vitest)🧪☕🧾

💡ここでは「Order が Aggregate Root」って前提で、 addItem / removeItem / changeQuantity / confirm / pay / cancel / fulfill みたいなメソッドがある想定にするね✨ (中身の実装は第56〜57章で作った子たち!)

✅ テストコード例(まずは王道:不変条件と遷移)

import { describe, it, expect } from "vitest";
import { OrderFixture } from "../__fixtures__/OrderFixture";
import { DomainError } from "../../domain/errors/DomainError";

describe("Order Aggregate 🏯", () => {
it("明細が空の注文は確定できない 😵‍💫", () => {
const order = OrderFixture.draft({ items: [] });

expect(() => order.confirm()).toThrowError(DomainError);
});

it("下書きの間は明細を追加でき、合計が更新される ✅💴", () => {
const order = OrderFixture.draft({ items: [] });

order.addItem(OrderFixture.line({ priceYen: 500, qty: 2 })); // 1000円
order.addItem(OrderFixture.line({ priceYen: 300, qty: 1 })); // +300円

expect(order.totalYen()).toBe(1300);
});

it("確定後は明細を追加できない 🔒", () => {
const order = OrderFixture.draft({ items: [OrderFixture.line({ priceYen: 500, qty: 1 })] });
order.confirm();

expect(() => order.addItem(OrderFixture.line({ priceYen: 300, qty: 1 }))).toThrowError(DomainError);
});

it("支払いは確定済みの注文だけできる 💳", () => {
const order = OrderFixture.draft({ items: [OrderFixture.line({ priceYen: 500, qty: 1 })] });

expect(() => order.pay()).toThrowError(DomainError);

order.confirm();
order.pay();

expect(order.status()).toBe("Paid");
});

it("支払い後はキャンセルできない 🚫", () => {
const order = OrderFixture.confirmedWithItems();
order.pay();

expect(() => order.cancel()).toThrowError(DomainError);
});

it("支払い後に fulfill できる ☕📦", () => {
const order = OrderFixture.confirmedWithItems();
order.pay();
order.fulfill();

expect(order.status()).toBe("Fulfilled");
});
});

ここでのポイント🧡✨

  • 「できないこと」をテストしてる(confirm できない、追加できない、pay できない、cancel できない)🔒
  • 例外の型は DomainError でまとめると、テストも読みやすいよ🧯
  • totalYen() みたいな “観測メソッド” があるとテストがスッキリ✨ (ドメインの内部配列を覗かないで済む👀🚫)

状態遷移は “パラメータ化” で一気に固める🧷🚦✨

Vitest は test.each / it.each が使えるよ(Jest互換)🧪 (Vitest) これを使うと、遷移の「禁止表」をそのままテストにできるのが気持ちいい〜💕

✅ 例:禁止遷移をまとめてテスト

import { describe, it, expect } from "vitest";
import { OrderFixture } from "../__fixtures__/OrderFixture";
import { DomainError } from "../../domain/errors/DomainError";

describe("Order state transitions 🚦", () => {
it.each([
["Draft", "pay", () => OrderFixture.draftWithItems()],
["Draft", "fulfill", () => OrderFixture.draftWithItems()],
["Confirmed", "fulfill", () => OrderFixture.confirmedWithItems()],
["Paid", "confirm", () => OrderFixture.paidWithItems()],
["Paid", "cancel", () => OrderFixture.paidWithItems()],
] as const)(
"%s 状態のとき %s はできない 🚫",
(_status, action, factory) => {
const order = factory();

expect(() => {
// action を呼ぶ
if (action === "pay") order.pay();
if (action === "fulfill") order.fulfill();
if (action === "confirm") order.confirm();
if (action === "cancel") order.cancel();
}).toThrowError(DomainError);
}
);
});

これの良さ😍

  • 遷移が増えても 配列を増やすだけ
  • テスト名が 仕様の一覧になる
  • 「抜け」が見つけやすい(禁止表に穴があると気づく)🕳️👀

“時間” が絡むなら:fake timers / Date 操作⏰🧪

もし集約に「期限」「締切」「キャンセル可能時間」みたいなルールが入るなら、 テストでは 時間を固定して安定させよ〜!✨

Vitest は vi.useFakeTimersvi.setSystemTime が用意されてるよ⏰🧊 (Vitest)

(このロードマップだと第86章の Clock 注入につながってくるやつだね💖)


🤖 AI活用(Copilot / Codex)で “テスト観点の漏れ” を潰すテンプレ✨

AIはコード生成より、観点チェックが強いよ〜!👀🧠

① 観点を増やしてもらうプロンプト📝✨

  • 「この集約の公開メソッド一覧」と「不変条件一覧」を貼って
  • こう聞く:

「この集約の不変条件が破れる “禁止操作の順序” を、テストケースとして20個提案して。 それぞれ Given/When/Then で書いて。重複はまとめて。」

② パラメータ化候補を作らせる🧷

「下の遷移表を、Vitest の it.each に落とし込む配列(ケース一覧)にして。 できればテスト名も読みやすくして。」

③ 例外メッセージの品質🧯

「DomainError のメッセージを、利用者向けと開発者向け(ログ向け)に分けたい。 例を5パターン作って。」


よくある失敗(集約テストあるある)😂⚠️

  • 内部配列を直接触るテスト(集約ルールを回避してしまう)
  • “最終状態だけ” 見て安心する(途中の禁止ルートが穴になる)
  • テストデータ生成がつらくて、ケース数が増えない(Fixture不足)
  • 例外が雑で、何がダメなのか分からない(DomainError設計が弱い)

仕上げ:この章のチェックリスト✅💖

テストが揃ったら、これ全部 YES なら勝ち〜!🎉✨

  • ✅ 不変条件は「守れる」だけじゃなく「破れない」を確認してる?🔒
  • ✅ 遷移は “できる” と “できない” が両方テストされてる?🚦
  • ✅ 代表的な順序違い(confirm 前に pay 等)を潰してる?🧯
  • ✅ テストが公開メソッドだけで書けてる?👆
  • ✅ Fixture があって、ケース追加がラク?🧁

ミニ演習🎓☕✨(次の章に効くやつ)

  1. 「禁止遷移表」をあなたの Order 集約で作ってみてね🚦📝
  2. それを it.each でテストにしてみてね🧷🧪
  3. AIに「抜けてる禁止操作の順序」を10個出させて、追加してみてね🤖✨

必要なら、あなたの第56〜57章の Order 実装(メソッド名や状態名)が分かる形で貼ってくれたら、そのコードにピッタリ一致する “第58章完成版のテスト一式”(Fixture込み)に整えて出すよ〜!🧪💖