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

第19章:テスト設計(レイヤードのご褒美🍰)🧪✨

レイヤードってね、うまく作れると「テストが超書きやすい」っていうご褒美が来るの〜!🥳🎁 この章は、そのご褒美の受け取り方(=どうテスト設計するか)を、ふんわり→具体で掴んでいくよ🫶💕


0. この章のゴール🎯✨

終わるころには、こんな状態になってるのが理想💖

  • Domain は “ほぼ純粋関数” みたいに、爆速でユニットテストできる🚀💎
  • Application は “Port(interface)をFakeに差し替え” してユースケースを検証できる🧸🔌
  • Infrastructure は “少数精鋭” の統合テストで守れる🛡️🔧
  • ✅ 「どこをどのテストで守る?」を迷わない🗺️✨

1. まずは全体像:テストの分担表🧁✨

Test Strategy

レイヤードは「責務が分かれてる」から、テストも分けやすいのが強みだよ😊

テストの基本方針(ざっくり)

  • 🧪 Domain:数が多くてOK(速い・壊れにくい)
  • 🎮 Application:ユースケース中心(Fake/Mockで外部を切る)
  • 🗄️ Infrastructure:少数精鋭(遅い&壊れやすいので最小に)
  • 🎛️ Presentation:薄いから “薄いことの確認” だけ(多すぎ注意⚠️)

テストピラミッド(イメージ)🏔️

  • 🔺上:E2E(少なめ)
  • 🔷中:統合テスト(少なめ)
  • 🟩下:ユニットテスト(多め)

2. テストランナー何にする?(2026の現実ライン)🧰✨

ここ、最新版の事情込みで “迷いにくい選択肢” を出すね😉

選択肢A:Vitest(いま超人気枠)⚡

  • Viteベースで、Viteの設定や解決を再利用しやすい🧠✨(Viteを使ってなくてもOK) (vitest.dev)
  • Vitest 4 では Browser Mode が安定化したり、ビジュアル回帰テストや Playwrightトレース対応が進んでるよ🧪🌐 (vitest.dev)
  • Watch運用が基本で開発体験が良い(ローカルで気持ちいい)👀✨ (vitest.dev)

選択肢B:Jest(安定の王道)👑

  • Jestの安定版は30系(公式に “Current version (Stable)” として案内) (jestjs.io)
  • 既存資産がある・チーム標準がJestなら全然アリ👍

選択肢C:Node標準のnode:test(依存を増やしたくない派)🟢

  • Nodeの標準テストランナー(node:test)は Node v20 で stable 扱い になってるよ🧩 (Node.js)

この章のサンプルは “Vitestで行く” 感じで書くね(理由:速い・書きやすい・今どき)⚡🫶 でも考え方は Jest / node:test でも同じだよ!


3. 最小セットアップ(Vitest)🛠️✨

3-1. まず入れるもの📦

  • vitest(本体)
  • カバレッジ(V8 providerが手軽✨) Vitestはv8/istanbul等のカバレッジ方式をサポートしてるよ📊 (vitest.dev) V8 providerのパッケージは @vitest/coverage-v8(直近だと4.0.17が案内されてる) (npm)
npm i -D vitest @vitest/coverage-v8

3-2. package.json の scripts(例)🧪

{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}

3-3. “UIでテスト見たい派” のおまけ🎀

VitestにはテストUIもあるよ〜!見ながら実行できて楽しい👀✨ (vitest.dev)

npm i -D @vitest/ui
npm run test -- --ui

4. Domainのテスト:ここが一番おいしい🍰💎

Domainは「外部に触れない」ように作ってるはずだから、テストが超ラク!✨ モック不要I/Oなし速い壊れにくい の四天王👑👑👑👑

ここでは ToDo を例にするね📝💕

4-1. 例:ValueObject(タイトルは空禁止!)🛑

「無効な状態を作れない」をテストで確認するよ😊

// src/domain/TodoTitle.ts
export class TodoTitle {
private constructor(public readonly value: string) {}

static create(raw: string): TodoTitle {
const v = raw.trim()
if (v.length === 0) throw new Error("TITLE_EMPTY")
if (v.length > 50) throw new Error("TITLE_TOO_LONG")
return new TodoTitle(v)
}
}
// src/domain/TodoTitle.test.ts
import { describe, it, expect } from "vitest"
import { TodoTitle } from "./TodoTitle"

describe("TodoTitle", () => {
it("空白だけはダメ🙅‍♀️", () => {
expect(() => TodoTitle.create(" ")).toThrow("TITLE_EMPTY")
})

it("前後の空白はトリムされる✂️", () => {
const t = TodoTitle.create(" hello ")
expect(t.value).toBe("hello")
})

it("長すぎはダメ📏", () => {
expect(() => TodoTitle.create("a".repeat(51))).toThrow("TITLE_TOO_LONG")
})
})

Domainテストのコツ3つ🧠✨

  • 🎯 “ルール(不変条件)” をテスト名に書いちゃう
  • 🧪 例外/エラーも「仕様」なので、ちゃんと確認する
  • 🧊 テストデータは少量でOK(速度優先!)

5. Applicationのテスト:Fakeでユースケース検証🎮🧸

Applicationは「手順の層」だから、テストはこうなるよ👇

  • ✅ 入力を受けて
  • ✅ Domainを使って
  • ✅ Port(Repositoryなど)へ依頼して
  • ✅ 結果DTOを返す

ここで大事なのは、外部(DB/HTTP)を動かさないこと! PortをFakeに差し替えれば、ユースケースだけを気持ちよく検証できる😊🔌

5-1. Port(interface)とUseCase(例)🔌🎮

// src/application/ports/TodoRepository.ts
export type TodoRecord = { id: string; title: string; done: boolean }

export interface TodoRepository {
save(todo: TodoRecord): Promise<void>
findById(id: string): Promise<TodoRecord | null>
}
// src/application/AddTodoUseCase.ts
import { TodoTitle } from "../domain/TodoTitle"
import { TodoRepository } from "./ports/TodoRepository"

export class AddTodoUseCase {
constructor(private readonly repo: TodoRepository) {}

async execute(input: { id: string; title: string }) {
const title = TodoTitle.create(input.title) // Domainルール
const todo = { id: input.id, title: title.value, done: false }
await this.repo.save(todo)
return { id: todo.id, title: todo.title, done: todo.done }
}
}

5-2. Fake Repository(インメモリ)🧸

// src/application/__tests__/FakeTodoRepository.ts
import { TodoRepository, TodoRecord } from "../ports/TodoRepository"

export class FakeTodoRepository implements TodoRepository {
private store = new Map<string, TodoRecord>()

async save(todo: TodoRecord): Promise<void> {
this.store.set(todo.id, todo)
}

async findById(id: string): Promise<TodoRecord | null> {
return this.store.get(id) ?? null
}
}

5-3. UseCaseテスト🎮✅

// src/application/AddTodoUseCase.test.ts
import { describe, it, expect } from "vitest"
import { AddTodoUseCase } from "./AddTodoUseCase"
import { FakeTodoRepository } from "./__tests__/FakeTodoRepository"

describe("AddTodoUseCase", () => {
it("ToDoを追加できる🎉", async () => {
const repo = new FakeTodoRepository()
const uc = new AddTodoUseCase(repo)

const out = await uc.execute({ id: "t1", title: "Buy milk" })

expect(out).toEqual({ id: "t1", title: "Buy milk", done: false })
expect(await repo.findById("t1")).toEqual({ id: "t1", title: "Buy milk", done: false })
})

it("タイトルが空なら失敗する😇(Domainルール)", async () => {
const repo = new FakeTodoRepository()
const uc = new AddTodoUseCase(repo)

await expect(uc.execute({ id: "t2", title: " " })).rejects.toThrow("TITLE_EMPTY")
})
})

ここがポイント💡

  • UseCaseテストは「DBがどう保存するか」じゃなくて、**“ユースケースとして正しいか”**を見る👀✨
  • Fakeを使うとテストが 速い&安定 になる🧸💕

6. Mock/Spyはどこで使う?(使いすぎ注意⚠️)🧩

Vitestには vi というヘルパがあって、Mock/Spyもできるよ🕵️‍♀️✨ (vitest.dev)

でも初心者のうちは、まずこう考えると失敗しにくいよ👇

  • 🧸 Fake:状態を持てる代役(インメモリRepoなど)→ よく使う
  • 🕵️ Spy:呼ばれた回数や引数を見たい → たまに使う
  • 🎭 Mock:振る舞いをガッツリ偽装 → 最終手段(増えると読みにくい)

7. Infrastructureのテスト:少数精鋭で守る🗄️🔧

Infrastructureは “外部の都合” が入るから、テストが遅くなりがち&壊れやすい🥲 だから 「最小限だけ」 にするのがコツ!

7-1. Repository実装の統合テスト(例)🧪

  • ✅ “保存して→読み出せる”
  • ✅ “マッピングが壊れてない” この2つが確認できれば十分なことが多いよ😊

7-2. 外部APIはMSWで安定化📡🛡️

外部APIテスト、直叩きすると落ちやすい(ネットワーク、レート制限、相手の障害…)😭 そこで MSW:リクエストを横取りして、モックレスポンスを返せる仕組みだよ✨ Nodeでも使えて、VitestのNodeテストでのセットアップも公式に案内されてる🧸📦 (mswjs.io)

(超ざっくり手順)

npm i -D msw

MSWは「実際に発生したリクエスト」を横取りする思想なので、テストが現実に寄りやすくて良い感じだよ🫶 (mswjs.io)


8. Presentationのテスト:薄いことを確認する🎛️✨

Presentationは理想としてこう👇

  • 入力を受ける
  • 形式チェック(軽め)
  • UseCase呼ぶ
  • 結果を返す(整形)

なのでテストも「変換が壊れてない」「UseCaseに渡せてる」を軽く確認するくらいでOK🙆‍♀️ (ビジネスルールのテストは Domain/Application に寄せる💎🎮)


9. カバレッジ:数字より “守れてる感” を優先📊💖

カバレッジは便利だけど、数字を上げるためのテストになった瞬間に負けやすい🥲 Vitestは v8/istanbul 等でカバレッジ取得ができるよ📈 (vitest.dev)

おすすめの使い方はこれ👇

  • ✅ DomainとUseCaseの “大事な分岐” が抜けてないか見る
  • ✅ 「ここ守れてないね」を発見するレーダーにする📡
  • ❌ 100%を目的にしない(コスパ悪化しがち)

10. ちょい上級:Property-based Testing(余裕が出たら)🎲✨

「境界値いっぱいある〜😵」ってなったら、fast-check が便利! fast-check は TypeScript/JavaScript の Property-based Testing ライブラリで、いろんなテストランナーで使えるよ🎲🧪 (Fast Check) Vitest用の統合パッケージもあるよ〜! (npm)

イメージ:

  • 「どんな入力でも、この性質は必ず成り立つ」 をテストする感じ😊

11. AI活用🤖✨(この章、めちゃ相性いい!)

AIはテストで特に強いよ〜!💪💕

11-1. 使える頼み方(コピペでOK)📝

  • 「このDomainの不変条件から、テスト観点を10個列挙して🙂」
  • 「このUseCase、Fakeでテスト書いて。AAAで、成功/失敗ケースもお願い🙏」
  • 「この関数、境界値テストの候補を全部出して〜📏」
  • 「このテスト、読みやすい名前にして✨(日本語でもOK)」

11-2. 注意点⚠️

  • AIが作るテストは 意図がズレることがあるので、最後は人間が「仕様」を握る🤝💖
  • “実装の詳細” をテストしすぎない(リファクタで折れる)😇

12. 演習(この章のメイン✨)🧩🎉

演習A:Domainのテストを3本追加💎

TodoTitleに次を足してテストしてみて😊

  • 「禁止文字」ルール(例:改行NG)
  • 「先頭の#は禁止」みたいなルール
  • 正常系の代表値(短い/長いギリギリ)

演習B:UseCaseに “重複ID禁止” を追加🎮

  • repo.findById を使って、存在したらエラーにする
  • テストは「既にあるIDで追加したら失敗」を書く🧸💥

演習C:外部API呼び出しをMSWでテスト📡

  • 外部レスポンス→Domain型へ変換する処理を作る
  • MSWで成功/失敗レスポンスを作って、変換とエラー処理を確認🛡️✨ (mswjs.io)

13. まとめチェック✅✨(合格ライン)

  • ✅ Domainは “外部なし” のユニットテストが中心になってる?💎
  • ✅ UseCaseは Port を Fake にして “手順が正しい” を見れてる?🎮
  • ✅ Infrastructureは “少数精鋭” で重要な接続だけ守れてる?🗄️
  • ✅ Presentationは薄く、ビジネスルールを持ってない?🎛️
  • ✅ テストがリファクタの味方になってる?(敵じゃない?)🫶

次の章(第20章)では、このテストたちを「運用で壊れにくくする仕組み(CI・依存ルール自動化・観測)」に繋げて、さらに強くしていくよ〜!🌱🏗️✨