第15章:テスト戦略(SoCがあると楽になる)🧪🌸✨
この章は「テストのやり方」だけじゃなくて、SoC(関心の分離)があると、テストが“めちゃ簡単になる”理由を体感する回だよ〜!🥳💕
15.0 まずゴール確認🎯✨
この章を終えたら、こうなってたら勝ち✅
- 「ここはユニットテスト」「ここは結合テスト」「ここはE2E」って迷わず仕分けできる🧠✨
- 純粋ロジックは爆速でテストできる(しかも壊れにくい)⚡✅
- I/O(通信・DB・時刻…)は薄く・必要最小限で守る👌
- AI(Copilot/Codex)でテストケースの穴を埋めるのが上手くなる🤖🧪
15.1 テストって何のため?(いちばん大事)💡💖
テストの目的は、ざっくりこれ👇
-
変更しても大丈夫って思える安心🛡️
-
バグを早めに見つけるレーダー📡
-
**仕様のメモ(生きたドキュメント)**📘✨
-
そして裏テーマ:設計のフィードバック装置🔧
- テストしにくい=責務が混ざってるサイン😇
- テストしやすい=分離できてるサイン💮
15.2 3種類だけ覚えよ!テストの基本セット🍱✨
① ユニットテスト(最優先🔥)✅
- 対象:Domainの純粋ロジック(計算・判定・ルール)🧠
- 特徴:速い・安定・壊れにくい⚡
- SoC的には:副作用ゼロの場所が主戦場🧼✨
② 結合テスト(薄くでOK👌)🔗
- 対象:Application + Adapterのつなぎ目(Repository/HTTP/DBなど)🌐
- 特徴:遅くなる&壊れやすいので最小限がコツ🥺
③ E2E(少数精鋭🏆)🎭
- 対象:ユーザー操作の“最重要ルート”だけ🧭
- 特徴:いちばん遅い&フレーキーになりがち😵💫
- でも:最後の砦なのでゼロは危険⚠️
15.3 SoCがあると、テストの置き場が決まる📦✨

SoCの3層(UI / Application / Domain)に当てはめると、こんな感じ👇
| 層 | 何を守る? | テストの主戦場 |
|---|---|---|
| Domain🛡️ | ルール・不変条件 | ユニットテスト大量✅ |
| Application🧭 | 手順・ユースケース | ユニット+軽い結合 |
| UI🖥️ | 表示と入力 | コンポーネントテスト or E2E少数 |
この表が頭に入ると、「どこまでテストする?」で迷いにくいよ〜!🥰
15.4 2026の“鉄板ツール構成”🧰✨(最新版ベース)
ユニット〜軽い結合:Vitest 4 系が強い⚡🧪
定番枠:Jest 30(安定・実績)🧪
- Jestは30.0が安定版として案内されてる (jestjs.io)
E2E:Playwright(TS相性よし)🎭✨
npm init playwright@latestでTypeScript前提の導線が用意されてる (Playwright)- Playwrightは1.57のリリースノートが先頭にあり、テスト可視化(Speedboard)とかも進化してる (Playwright)
- さらにPlaywright Test Agentsみたいに「LLMにテスト生成〜修復を誘導する仕組み」も入ってきてて、AI前提運用と相性◎ (Playwright)
通信モック:MSW v2(実リクエストを横取り系)🕸️
- Vitest向けの案内もある(NodeテストでHTTPを横取り) (mswjs.io)
15.5 まず“超小さい題材”で分離→テストしよ☕🧁
ここからミニ題材でいくよ〜! 「カフェの注文で割引計算」☕🍰(Domainが主役)
✅ Domain:純粋ロジック(副作用なし)
例:合計金額・割引・端数処理みたいなやつ✨
// src/domain/pricing.ts
export type Money = number;
export function calcTotal(price: Money, qty: number): Money {
if (!Number.isInteger(qty) || qty <= 0) throw new Error("qty must be positive int");
if (price < 0) throw new Error("price must be >= 0");
return price * qty;
}
export function applyStudentDiscount(total: Money): Money {
// 学割:1000円以上なら10%OFF(例)
if (total < 0) throw new Error("total must be >= 0");
return total >= 1000 ? Math.floor(total * 0.9) : total;
}
✅ ユニットテスト:ここが“爆速で気持ちいい”ゾーン⚡🧪
// src/domain/pricing.test.ts
import { describe, it, expect } from "vitest";
import { calcTotal, applyStudentDiscount } from "./pricing";
describe("pricing", () => {
it("calcTotal: price * qty", () => {
expect(calcTotal(300, 2)).toBe(600);
});
it("calcTotal: qtyが不正ならエラー", () => {
expect(() => calcTotal(300, 0)).toThrow();
expect(() => calcTotal(300, 1.2)).toThrow();
});
it("applyStudentDiscount: 1000未満はそのまま", () => {
expect(applyStudentDiscount(999)).toBe(999);
});
it("applyStudentDiscount: 1000以上は10%OFFで切り捨て", () => {
expect(applyStudentDiscount(1000)).toBe(900);
expect(applyStudentDiscount(1111)).toBe(999);
});
});
ここまで来たらもうSoCの恩恵出てるよ🥹💕 UIも通信もDBも関係なく、ルールが守れてるか一瞬で確認できる✅
15.6 Application:依存を“注入”すると、I/Oが怖くなくなる💉✨
次に「注文を保存する」みたいな流れ(ユースケース)を作るよ🧭 この層は Repository(保存先)をインターフェース化して、テストでは偽物に差し替えるのがコツ!
// src/application/placeOrder.ts
import { calcTotal, applyStudentDiscount } from "../domain/pricing";
export type Order = { itemId: string; price: number; qty: number; isStudent: boolean };
export interface OrderRepository {
save(order: { itemId: string; qty: number; total: number }): Promise<void>;
}
export async function placeOrder(repo: OrderRepository, order: Order): Promise<number> {
const base = calcTotal(order.price, order.qty);
const total = order.isStudent ? applyStudentDiscount(base) : base;
await repo.save({ itemId: order.itemId, qty: order.qty, total });
return total;
}
✅ Applicationのテスト:Fake repoでOK🙆♀️✨
// src/application/placeOrder.test.ts
import { describe, it, expect } from "vitest";
import { placeOrder, type OrderRepository } from "./placeOrder";
describe("placeOrder", () => {
it("学割なら割引後のtotalを保存する", async () => {
const saved: any[] = [];
const repo: OrderRepository = {
async save(x) { saved.push(x); }
};
const total = await placeOrder(repo, { itemId: "coffee", price: 500, qty: 2, isStudent: true });
expect(total).toBe(900);
expect(saved).toEqual([{ itemId: "coffee", qty: 2, total: 900 }]);
});
});
ここ大事ポイント📌
- Domainは純粋だから速い
- Applicationは依存を注入するからテストしやすい
- DBやHTTPが絡むのは、もっと外側(Adapter)に追い出す✨
15.7 I/O(通信)をテストするなら:MSWで“現実に近いモック”🕸️✨
「fetchをモックしたい…」ってとき、MSWはかなり便利寄り🧡 NodeテストでHTTPリクエストを横取りする導線が公式にあるよ (mswjs.io)
使いどころのコツ👇
- Domain / Application:基本モック不要(注入で解決)✅
- Adapter(APIクライアント):ここだけMSWで“薄く”結合テスト👌
15.8 E2Eは「最重要ルートだけ」Playwrightで守る🎭🏆
E2Eは増やしすぎると地獄になりやすい😇 だから方針はこれ👇
- ✅ “お金が動く”/“申し込みが完了する”/“ログインできる” 最重要ルートだけを少数精鋭で守る🏆
Playwrightはセットアップ導線も整ってるし (Playwright)、 最近はテスト可視化(Speedboard)とかも強化されてるよ (Playwright)
そしてAI前提ならここ超アツ🔥
- Playwright Test Agents(planner / generator / healer)が入ってきて、 テスト計画→生成→修復をLLMに誘導できる設計になってる (Playwright)
15.9 「どこまでテストする?」迷った時の3問クイズ🎯🧠
迷ったらこれを自問してね👇
- それ壊れたら痛い?(お金・信用・作業ロス)💥
- 仕様がよく変わる?(UIは変わりやすい)🔁
- 遅い/不安定になりそう?(E2E増やしすぎ注意)🐢😵💫
ざっくり結論🍬
- 壊れたら痛いルール → Domainでユニットテスト厚め✅
- I/Oや外部連携 → 結合テスト薄め👌
- 最重要の体験 → E2E少数🏆
15.10 AI(Copilot/Codex)に頼むと強いプロンプト例🤖🪄
そのまま貼ってOK系だよ〜!💕
- 「この関数の境界値/異常系のテストケースを10個出して」🧪
- 「この処理がテストしにくい。SoCを守る分割案を3案出して」✂️
- 「RepositoryをDIできる形に直して。差分最小で」🔧
- 「Playwrightでこの画面の最重要ルートE2Eを1本だけ提案して」🎭
- 「失敗したE2Eログから、原因候補と直し方を3つ」🩹
15.11 章末ミッション(手を動かすやつ)🏁✨
ミッションA(ユニット)🧼🧪
applyStudentDiscountに仕様追加👇
- 合計が 3000円以上なら 15%OFF(それ以外は従来どおり) → テストを先に追加してから実装してね(テスト駆動ごっこ💖)
ミッションB(Application)🧭🧪
placeOrderに「在庫チェック」を追加したい!
StockService(interface)を新設して、placeOrderに注入✨- テストではFakeを作って「在庫なしならエラー」を確認✅
ミッションC(E2E)🎭🏆
画面がある想定で、
- 「注文して完了メッセージを見る」E2Eを1本だけ作る✨ (1本でいい!増やさない!えらい!🥹)
15.12 今日のまとめ(これだけ覚えよ)💮✨
- SoCができてると、Domainはユニットテストが超ラク🧠✅
- I/Oは外側に寄せて、薄い結合テストで守る🌐👌
- E2Eは最重要ルートだけを少数精鋭で🎭🏆
- 2026は、Vitest 4系・Jest 30・Playwright 1.57あたりが軸になりやすい流れだよ (Vitest)
次の第16章は、いよいよ **「ごちゃ混ぜ→分離してスッキリ」**の大型Before/Afterケーススタディだよ〜📚🔥 この第15章で作ったテスト戦略が、分離リファクタの安心材料になるから超大事!🧪🛡️