第59章:Strategy ③ まとめ:テストが超簡単になる🧪🎉
ねらい🎯
- Strategy(戦略)を「純粋関数」に寄せて、戦略ごとに独立テストできるようになる✨
- 代表ケース+境界値で「壊れてない安心感」を作れる✅
- 戦略がI/O(API/DB)に近づきそうなときの分離ラインがわかる🚧
最新情報メモ🗞️(2026-02時点)
- TypeScript は npm 上の “Latest” が 5.9.3 として公開されているよ📌 (npmjs.com)
- TypeScript 6.0 は 5.9 と 7.0 の橋渡し(非推奨整理など)という位置づけが語られているよ🧭 (Microsoft for Developers)
- Node の組み込みテスト(
node:test)は Node 20 で stable(標準で使える)🧪 (nodejs.org) - 2026-02 頃の Node リリースラインだと v24 が LTS / v25 が Current になってるよ🔁 (nodejs.org)
- テスト基盤としては Vitest v4 系も普及していて、機能拡張が進んでる(選択肢の1つ)🧰 (void(0))
1) 今日の結論💡「Strategyが純粋なら、テストは呼ぶだけ」
Strategy をこうする👇
- 入力:注文データ(order)と計算済み小計(subtotal)
- 出力:割引額(または最終金額)
- ルール:副作用なし(ログ出力しない / Date.now() 使わない / 外部アクセスしない)
こうなるとテストは超シンプル🎉
- 「この注文を入れたら割引いくら?」を assert するだけ🧪

2) 題材☕:割引 Strategy を3つ作ってテストする
ここでは「円(整数)」で扱うよ💴(小数の丸め地獄を避けるため😇)
戦略の例🧁
- 👩🎓 学割:10% OFF(上限 500円)
- 🥇 ゴールド会員:いつでも 5% OFF(上限なし)
- 🕒 ハッピーアワー:15:00〜16:59 は 200円 OFF(ただし小計 1000円以上)
3) 実装:Strategy を “関数+Map登録” にする🗂️✨
src/discount.ts
export type Item = {
name: string;
priceYen: number; // 円(整数)
qty: number; // 個数(整数)
};
export type MemberTier = "guest" | "silver" | "gold";
export type Order = {
items: Item[];
memberTier: MemberTier;
isStudent: boolean;
createdAt: Date; // 「いつ注文したか」を外から注入(Date.now()禁止)
};
export type DiscountKey = "none" | "student" | "gold" | "happyHour";
/**
* Strategy:割引額(円)を返す
* - 副作用なし
* - 入力が同じなら出力も同じ
*/
export type DiscountStrategy = (order: Order, subtotalYen: number) => number;
export function calcSubtotalYen(order: Order): number {
const subtotal = order.items.reduce((sum, it) => {
assertNonNegativeInt(it.priceYen, "priceYen");
assertNonNegativeInt(it.qty, "qty");
return sum + it.priceYen * it.qty;
}, 0);
return subtotal;
}
export function calcTotalYen(order: Order, key: DiscountKey): number {
const subtotal = calcSubtotalYen(order);
const strategy = getDiscountStrategy(key);
const discount = clampYen(strategy(order, subtotal), 0, subtotal);
return subtotal - discount;
}
/** Strategy 登録(Map) */
const strategies = new Map<DiscountKey, DiscountStrategy>([
["none", () => 0],
["student", (order, subtotal) => {
if (!order.isStudent) return 0;
const raw = Math.floor(subtotal * 0.1);
return Math.min(raw, 500);
}],
["gold", (order, subtotal) => {
if (order.memberTier !== "gold") return 0;
return Math.floor(subtotal * 0.05);
}],
["happyHour", (order, subtotal) => {
if (subtotal < 1000) return 0;
const h = order.createdAt.getHours();
// 15:00〜16:59
const isHappy = h === 15 || h === 16;
return isHappy ? 200 : 0;
}],
]);
export function getDiscountStrategy(key: DiscountKey): DiscountStrategy {
return strategies.get(key) ?? strategies.get("none")!;
}
/** ちょいユーティリティ(独自クラスは作らない) */
function clampYen(n: number, min: number, max: number): number {
if (!Number.isFinite(n)) return min;
return Math.max(min, Math.min(max, Math.floor(n)));
}
function assertNonNegativeInt(n: number, name: string): void {
if (!Number.isInteger(n) || n < 0) {
throw new Error(`${name} must be a non-negative integer. got=${n}`);
}
}
ポイント✅
- Strategy は 関数(
DiscountStrategy) - 追加は
strategies.set("newKey", fn)でOK🗂️ createdAtを Order に持たせて、時間判定も テスト可能にしてる🕒
4) テスト:Node標準の node:test で戦略ごとにサクッと🧪✨
Node の組み込みテストは node:test を使うよ(Node 20 で stable)(nodejs.org)
src/discount.test.ts
import test from "node:test";
import assert from "node:assert/strict";
import { calcTotalYen, type Order } from "./discount.js"; // NodeNext想定(.js参照)
function makeOrder(partial: Partial<Order> = {}): Order {
return {
items: [
{ name: "Latte", priceYen: 500, qty: 1 },
{ name: "Cookie", priceYen: 300, qty: 2 },
],
memberTier: "guest",
isStudent: false,
createdAt: new Date("2026-02-01T14:00:00+09:00"),
...partial,
};
}
test("none: 割引なし", () => {
const order = makeOrder();
// 小計 = 500*1 + 300*2 = 1100
assert.equal(calcTotalYen(order, "none"), 1100);
});
test("student: 学割10%(上限500円)", () => {
const order = makeOrder({ isStudent: true });
// 1100 * 10% = 110(端数切り捨て)
assert.equal(calcTotalYen(order, "student"), 990);
});
test("gold: ゴールド会員5%", () => {
const order = makeOrder({ memberTier: "gold" });
// 1100 * 5% = 55
assert.equal(calcTotalYen(order, "gold"), 1045);
});
test("happyHour: 15-16時は200円OFF(小計1000円以上)", () => {
const order = makeOrder({ createdAt: new Date("2026-02-01T15:30:00+09:00") });
assert.equal(calcTotalYen(order, "happyHour"), 900);
});
test("happyHour: 小計1000円未満なら0円", () => {
const order = makeOrder({
items: [{ name: "Tea", priceYen: 400, qty: 2 }], // 800
createdAt: new Date("2026-02-01T15:10:00+09:00"),
});
assert.equal(calcTotalYen(order, "happyHour"), 800);
});
5) 動かし方(TS→JSにして Node でテスト)🧰
Node のテストランナーは標準で使えるけど、TypeScript はそのままだと実行できないので、まず tsc で JS にするよ(いちばん堅い&標準寄り)✨
tsconfig.json(例)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"declaration": false,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}
package.json scripts(例)
{
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "npm run build && node --test dist/**/*.test.js"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}
node --test は Node の組み込み機能だよ🧪 (nodejs.org)
(--watch もあるけど、watch は一部 experimental 扱いの箇所があるので「便利だけど様子見」でOK👀)(nodejs.org)
6) 境界値テストの作り方🧊(Strategyはここが気持ちいい)
「戦略が壊れやすい境界」を狙うのがコツ🎯
よく効く境界値リスト🧪
- 💴 0円:小計0のとき(items空、qty=0)
- 🔢 ちょうど:ハッピーアワー開始時刻(15:00)、終了境界(16:59→17:00)
- 📏 しきい値:小計が 999 / 1000 のとき
- 🧢 上限:学割の上限500円(小計が 4999 / 5000 / 5001 とか)
- 🧮 丸め:%計算の端数(1099円の10%は109円、1100円は110円)
- 🧨 異常系:priceYen や qty が負数・小数(ちゃんと落ちる?)
ここが最高ポイント💖 Strategy が純粋だと、「入力をちょっと変えて assert」だけで境界値が全部いける✨
7) つまずき回避💡(Strategyが“テストしにくい子”になる原因)
❌ ありがち地雷💣 → ✅ こう直す
- ❌
Date.now()を Strategy 内で使う → ✅createdAtを Order に入れて注入🕒 - ❌
console.logを Strategy に入れる → ✅ ログは外側(Decorator/呼び出し側)へ📣 - ❌ Strategy が API/DB を叩く → ✅ 「取得」と「計算」を分離して、計算だけ Strategy にする🧼
- ❌ Order を破壊的に変更する → ✅ できれば読み取り専用で扱う(必要ならコピー)🧊
8) 「戦略がI/Oし始めたら」分離ライン🚧(超大事)
たとえば「割引率が管理画面で変わる」みたいになると、Strategy が設定取得をしたくなるよね😵💫 でも Strategy は計算担当に固定するのが勝ち✨
形のイメージ🧠
- ✅
fetchDiscountConfig():外部I/O(API/DB) - ✅
calcDiscount(config, order, subtotal):純粋関数(Strategyにする) - ✅ テスト:
calcDiscountを全力でテストする🧪
これで「I/Oの不安定さ」と「割引計算の正しさ」が混ざらない🧼✨
9) AIプロンプト例🤖💬(テスト設計が一気に進む)
次の割引Strategy(純粋関数)について、代表ケース+境界値のテストケースを10個提案して。
- 入力: order, subtotalYen
- 出力: discountYen
- 仕様: 学割10%上限500円 / Gold5% / HappyHour(15-16時200円OFFかつ1000円以上)
- 出力形式: ケース名 / 入力 / 期待値 / そのケースが重要な理由
このStrategyが「純粋関数になっていない」疑いがある点をチェックして、改善案を3つ出して。
(Date.now, random, I/O, mutation なども観点に入れて)
node:test + assert/strict で、境界値テスト中心のテストコードを書いて。
(過剰なモックや独自テストユーティリティは作らない)
10) まとめ✅🎉
- Strategy を 関数にして Map登録すると、追加も整理もラク🗂️✨
- Strategy を 純粋関数に寄せると、テストは「呼んで assert」だけ🧪💖
- I/O が絡みそうになったら、「取得」と「計算」を分けて、計算だけを Strategy に残す🧼
次は「通知したい」を疎結合で実現する Observer に進むよ📣✨