第56章:卒業制作:小アプリを“TDDで完成”+CI導入✅

ここは“総まとめ回”だよ〜!🎉 **仕様→テスト→実装→リファクタ→運用(CI)**まで、1人で回して「作りきる」体験をするよ🧪💪
🧭 0) 今日の“最新版メモ”(2026/1/19時点)🗓️✨
- **Node.js:v24 が LTS(Active LTS)**になってるよ🟩(例:v24.13.0 が 2026-01-13 公開) (Node.js)
- TypeScript:v5.9.3が最新リリース(2025-10-01) (GitHub)
- Vitest:v4.0.17 が Latest(2026-01-12) (GitHub)
- GitHub Actions:actions/checkout は v6 系(例:v6.0.1 が 2025-12-02) (GitHub)
- actions/setup-node は v6 系で、依存キャッシュ(npm/yarn/pnpm)もサポートしてるよ🧰 (GitHub)
🎯 1) ゴール(この章の合格ライン)✅🎀
**“小さくても完成したアプリ”**を作るよ✨
✅ 合格の定義(Definition of Done)
- 重要ユースケース 3つが TDDで完成してる🧪
- ローカルで テスト&型チェックが安定して通る🔁
- GitHub Actions で テスト+型チェックが自動実行される🤖✅
- README に 実行手順・テスト手順・仕様の概要が書いてある📘
🧁 2) 作品テーマ(おすすめ3つ)🎨
どれでもOK!でもこの章では **例として「推し活グッズ管理🎀」**で進めるね😊
- 🎀 推し活グッズ管理:買ったもの/カテゴリ/合計金額
- 💰 かんたん家計簿:収支入力/月別集計/カテゴリ別
- 🍙 学食注文:注文カート/合計/注文確定(在庫は発展)
🧩 3) “重要ユースケース3つ”を決める(ここ超大事!)📝✨
まず 増やさないのがコツだよ〜!😵💫➡️😌
🎀 推し活グッズ管理(例)
- 追加する:名前・価格・カテゴリを入力して登録する➕
- 一覧する:カテゴリで絞って表示する📋
- 集計する:カテゴリ別の合計金額を出す📊
✅ 受け入れ条件(Given/When/Then)を1行ずつ書く
例:
- Given 正しい名前と価格, When 追加する, Then 一覧に増えている
- Given 価格がマイナス, When 追加する, Then エラーが返る(保存されない)
🏗️ 4) “崩れない形”の最小アーキ(超かんたん版)🧱✨
ポイントはこれだけ👇 ドメイン(中心のルール)を、UIやファイル保存から守る🛡️
src/
domain/ ← ルールと型(ここが主役✨)
usecases/ ← 3ユースケース(アプリの動き)
ports/ ← 依存の“口”(Repositoryなど)
adapters/ ← ファイル保存や in-memory 実装
ui/ ← CLIとか(最小でOK)
tests/
🧪 5) まずは「追加する」をTDDで完成させよう(例)🎀➕✅
📚 学ぶ
- “まずテストで仕様を固定”→最小実装→整理 の流れ
- 失敗(エラー)も仕様にする🚫✨
🧪 手を動かす:最小の型&Result(例)
// src/domain/result.ts
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
🧪 手を動かす:ユースケースのテスト(先に書く!)
// tests/addItem.test.ts
import { describe, it, expect } from "vitest";
import { addItem } from "../src/usecases/addItem";
import { InMemoryItemRepo } from "../src/adapters/inMemoryItemRepo";
describe("addItem", () => {
it("正しい入力なら保存され、okが返る🎀", async () => {
const repo = new InMemoryItemRepo();
const r = await addItem(
{ name: "アクスタ", priceYen: 2500, category: "goods" },
{ repo }
);
expect(r.ok).toBe(true);
if (r.ok) {
const all = await repo.listAll();
expect(all.length).toBe(1);
expect(all[0].name).toBe("アクスタ");
expect(all[0].priceYen).toBe(2500);
}
});
it("価格がマイナスならerrで、保存されない🚫", async () => {
const repo = new InMemoryItemRepo();
const r = await addItem(
{ name: "アクスタ", priceYen: -1, category: "goods" },
{ repo }
);
expect(r.ok).toBe(false);
const all = await repo.listAll();
expect(all.length).toBe(0);
});
});
✅ 最小実装(テストが通るだけでOK)
// src/ports/itemRepo.ts
export type Item = {
id: string;
name: string;
priceYen: number;
category: "goods" | "ticket" | "other";
};
export interface ItemRepo {
save(item: Item): Promise<void>;
listAll(): Promise<Item[]>;
}
// src/adapters/inMemoryItemRepo.ts
import { ItemRepo, Item } from "../ports/itemRepo";
export class InMemoryItemRepo implements ItemRepo {
private items: Item[] = [];
async save(item: Item) {
this.items.push(item);
}
async listAll() {
return [...this.items];
}
}
// src/usecases/addItem.ts
import { ItemRepo, Item } from "../ports/itemRepo";
import { Result, ok, err } from "../domain/result";
type AddItemInput = {
name: string;
priceYen: number;
category: Item["category"];
};
type AddItemError =
| { type: "InvalidName" }
| { type: "InvalidPrice" };
export async function addItem(
input: AddItemInput,
deps: { repo: ItemRepo }
): Promise<Result<Item, AddItemError>> {
if (input.name.trim().length === 0) return err({ type: "InvalidName" });
if (!Number.isInteger(input.priceYen) || input.priceYen < 0) {
return err({ type: "InvalidPrice" });
}
const item: Item = {
id: crypto.randomUUID(),
name: input.name,
priceYen: input.priceYen,
category: input.category,
};
await deps.repo.save(item);
return ok(item);
}
🧹 Refactor(この順でやると安全💡)
- テスト名を “仕様の文章” にする📝
Itemの生成をdomain/側に寄せる(入力チェックもそこへ)🧠AddItemErrorを増やしても壊れない形にする(unionの出番✨)
📋 6) ユースケース②「一覧する(絞り込み)」を追加🧪✨
🎯 仕様例
- カテゴリ指定があればそれだけ返す
- なければ全部返す
ここは パラメータ化テストが気持ちいいよ🔁💕
(例:[{ filter: "goods", expected: 2 }, ...] みたいに)
📊 7) ユースケース③「集計する(カテゴリ別合計)」🧪💰
🎯 仕様例
goods: 5000, ticket: 12000, other: 0みたいな集計結果- “順序”が仕様じゃないなら、オブジェクト比較でOK🧸
ここは「境界値」も入れやすい✨
- 0件
- 1件
- 同カテゴリ複数
- 大きい数(合計)
📁 8) (発展)ファイル保存を“アダプタ”で足す✨
やることは1つだけ👇 **ドメイン&ユースケースには触らない。Repositoryの実装だけ増やす。**🔌
FileItemRepoを作る(JSONで保存/読み込み)- ユースケースのテストは in-memory のまま(速い&安定⚡)
- ファイル保存は 統合テストを1〜2本だけ(少数精鋭⭐)
🤖 9) CI(GitHub Actions)を入れる✅🎉
📚 学ぶ
- push / PR のたびに テスト+型チェックが走る
- actions/setup-node は 依存キャッシュもできる🧰 (GitHub)
- checkout は v6 系が現行だよ📦 (GitHub)
🧪 例:.github/workflows/ci.yml
name: ci
on:
push:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
node: [24]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm test -- --run
Node は v24 が LTS(Active LTS)なので、この章は
24で固定しちゃうのがラクだよ🟩 (Node.js)
🧾 10) package.json の最小スクリプト例🧪✨
{
"type": "module",
"scripts": {
"test": "vitest",
"test:watch": "vitest --watch",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"devDependencies": {
"typescript": "^5.9.3",
"vitest": "^4.0.17"
}
}
(TypeScript v5.9.3 / Vitest v4.0.17 は本日時点の参照だよ。) (GitHub)
🤖 11) AIの使い方(この章専用テンプレ)📌✨
✅ 仕様を固める(最初だけ)
- 「ユースケース3つを Given/When/Then で1行ずつにして。抜けや曖昧さも指摘して」
✅ テストの観点抜けチェック(超便利)
- 「このテスト群の“抜け観点”を列挙して。追加するなら優先度順に3つ」
✅ リファクタ安全運転
- 「この変更を3コミットに分けるなら、分割案とそれぞれのテストの守り方は?」
✅ PRレビュー役(最後に強い)
- 「仕様に対してテストが弱いところ、命名が誤解されそうなところを厳しめに指摘して」
✅ 12) 提出物(これが揃ったら卒業🎓🎉)
-
✅ リポジトリ(ソース一式)
-
✅ テスト(ユースケース3つ+異常系)
-
✅ CI(Actionsが緑💚)
-
✅ README(1枚でOK)
- 何のアプリ?
- できること(ユースケース3つ)
- 実行方法 / テスト方法
- 仕様メモ(制約・今後の改善案)
✅ 最終チェックリスト(詰まったらここを見る)🧯✨
- 🧪 テストが落ちた:**仕様が変わった?実装が壊れた?**を先に分ける
- 🐢 遅い:ファイルI/Oがユニットに混ざってない?(混ざってたら境界へ🚪)
- 💥 たまに落ちる:時間・乱数・共有状態が混ざってない?(注入&固定!)
- 🧹 リファクタ怖い:テストを守りながら3回に分ける(命名→抽出→整理)
必要なら、次のどれかで「第56章」をさらに教材っぽく整えるよ😊💕
- ✅ 章内の“コミット単位”まで分解した進行表(1〜3コミット×数セット)
- ✅ 「推し活グッズ管理🎀」の完成版を、ユースケース3つぶんまるっとサンプル実装
- ✅ UI(CLI or ちいさなWeb)をどこまでやるか、最小の選び方ガイド🧭