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

第6章:テストと設計の関係(テストは味方)🧫✅✨

この章はね、ひとことで言うと… **「テストを書きやすいコード=だいたい設計が良い」**って感覚を手に入れる回だよ〜😊🌸

Class Library


1) この章でできるようになること🎯✨

  • 「テストしにくいコード」がなぜ設計的にツラいのか説明できる🧠💡
  • Vitestでテストを動かせるようになる🏃‍♀️💨(最近の最新版は 4系だよ)(npm)
  • ミニプロジェクトの「料金計算」を題材に、ユニットテストを1本以上書ける💰✅
  • 設計をちょい改善してテストしやすくする体験ができる🧼✨

2) なんで「テスト」が「設計」とつながるの?🔗🤔

✅ テストしにくいコードあるある😵‍💫

  • 入力がどこから来るのかわからない(グローバルや環境変数に依存)🌪️
  • 時刻(Date.now())や乱数(Math.random())で結果が毎回変わる🎲
  • DB/通信/ファイルアクセスが計算ロジックに混ざってる📡💥
  • 1つの関数がデカすぎて、どこを検証したいのか不明👀💦

✅ つまりこういうこと💡

テストって「このコード、外から見てちゃんと説明できる?」の確認作業なのね😊

  • 入力(引数)がハッキリしてて
  • 出力(戻り値)がハッキリしてて
  • 余計な副作用が少ないほど → テストが書きやすい✨ → 設計もスッキリしがち✨

3) テストの種類(超ざっくり)🧁📚

  • ユニットテスト:小さい部品(関数/クラス)を高速に検証⚡✅
  • 結合テスト:複数部品をつないで検証🔌✅
  • E2E:ユーザー操作に近い形で検証🧑‍💻✅

この章はまず ユニットテストに集中するよ〜!🧸✨


4) 今どきのテストツール:Vitestを使うよ🧡⚡

  • Vitestは Vite系の速さを活かしたテストランナーで、Jest互換のAPIも多いよ〜😊(Vitest)
  • 「expect」もJest互換の書き味があるから入りやすい👍✨(Vitest)
  • 参考までに:Jestの最新版は 30系(安定版が30)だよ〜🃏(Jest)

(TypeScript自体の最新版も 5.9系だね)(npm)


5) まずは最短で「テストを動かす」🧪🏃‍♀️

5-1) インストール(必要最低限)📦✨

npm i -D vitest @types/node

5-2) package.json にスクリプト追加📝

{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
  • npm test:監視モードでサクサク回る🌀✨
  • npm run test:run:CI向けに1回だけ実行🎯

6) ミニ題材:料金計算を「テストで守る」💰🛡️✨

ここからが本番〜! **「Campus Café 注文」**の料金計算を、テストで安心にしていくよ☕️📦✅

6-1) まず「テストしやすい形」にするコツ🍀

料金計算は、こういう形が最高にテスト向き😊✨

  • 入力:注文内容(商品と数量)
  • 出力:合計金額(数値)
  • 外部依存:なし(通信/DB/時刻を混ぜない)

7) 実装:calculateTotal を作る🧾✨

7-1) 実装ファイル:src/domain/priceCalculator.ts

※お金は 小数を避けて整数(円)で扱うのがおすすめだよ〜💴✨

export type Yen = number;

export type LineItem = {
name: string;
unitPrice: Yen; // 例: 480
quantity: number; // 例: 2
};

export type Discount = {
type: "none" | "percent" | "fixed";
value: number; // percentなら 10 (=10%)、fixedなら 100 (=100円引き)
};

function assertIntegerYen(value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new Error(`Yen must be a non-negative integer. got=${value}`);
}
}

function calcSubtotal(items: LineItem[]): Yen {
const subtotal = items.reduce((sum, item) => {
if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
throw new Error(`quantity must be positive integer. got=${item.quantity}`);
}
assertIntegerYen(item.unitPrice);
return sum + item.unitPrice * item.quantity;
}, 0);

assertIntegerYen(subtotal);
return subtotal;
}

function applyDiscount(subtotal: Yen, discount: Discount): Yen {
assertIntegerYen(subtotal);

if (discount.type === "none") return subtotal;

if (discount.type === "percent") {
// 例: 10%引き → subtotal * 0.9
const rate = discount.value / 100;
const discounted = Math.floor(subtotal * (1 - rate));
assertIntegerYen(discounted);
return discounted;
}

if (discount.type === "fixed") {
const discounted = Math.max(0, subtotal - discount.value);
assertIntegerYen(discounted);
return discounted;
}

// 将来discount.typeが増えても、TypeScriptがここを守ってくれる👍
const _exhaustive: never = discount.type;
return _exhaustive;
}

function addTax(amount: Yen, taxRate: number): Yen {
// taxRate: 0.1 (=10%) みたいな想定
if (taxRate < 0 || taxRate > 1) throw new Error(`taxRate must be 0..1 got=${taxRate}`);
assertIntegerYen(amount);

const taxed = Math.floor(amount * (1 + taxRate));
assertIntegerYen(taxed);
return taxed;
}

export function calculateTotal(
items: LineItem[],
discount: Discount,
taxRate: number
): Yen {
const subtotal = calcSubtotal(items);
const discounted = applyDiscount(subtotal, discount);
return addTax(discounted, taxRate);
}

8) テスト:まず1本!🎉✅

8-1) テストファイル:src/domain/priceCalculator.test.ts

import { describe, expect, test } from "vitest";
import { calculateTotal, type LineItem } from "./priceCalculator";

describe("calculateTotal", () => {
test("割引なし:小計に税を足す", () => {
const items: LineItem[] = [
{ name: "Café Latte", unitPrice: 480, quantity: 2 }, // 960
{ name: "Cookie", unitPrice: 220, quantity: 1 }, // 220
];

// 小計 1180、税10%→ 1298(小数切り捨て)
const total = calculateTotal(items, { type: "none", value: 0 }, 0.1);
expect(total).toBe(1298);
});
});

8-2) 実行🏃‍♀️💨

npm test

通ったら勝ち〜!🎊🥳✨


9) ちょい応用:表形式でテストする(増やしやすい)📋✨

Vitestは test.each みたいな Jest互換の書き方もできるよ〜😊(Vitest)

import { describe, expect, test } from "vitest";
import { calculateTotal, type LineItem } from "./priceCalculator";

describe("calculateTotal - table tests", () => {
const baseItems: LineItem[] = [
{ name: "Sandwich", unitPrice: 500, quantity: 1 } // 500
];

test.each([
["none", { type: "none", value: 0 }, 0.1, 550],
["10% off", { type: "percent", value: 10 }, 0.1, 495], // 500→450→税で495
["100yen off", { type: "fixed", value: 100 }, 0.1, 440], // 500→400→税で440
])("%s", (_name, discount, taxRate, expected) => {
const total = calculateTotal(baseItems, discount as any, taxRate);
expect(total).toBe(expected);
});
});

10) 「テストしにくい設計」を「テストしやすく」するミニ改善🧼✨

ありがちな悪い例(イメージ)😇💥

  • calculateTotal() の中で

    • DBから税率を取ってきたり📡
    • 今日の日付でキャンペーン判定したり🕒
    • ログ出したり🧾 → するとテストが一気にしんどい😵‍💫

いい分け方(超大事)🌈

  • domain(計算):純粋に「入力→出力」だけ
  • infra(取得/保存/通知):外部に触る
  • app(つなぎ役):必要なものを集めてdomainを呼ぶ

この分け方をすると、domainがテスト天国になるよ🏝️✨


11) カバレッジ(おまけだけど強い)🧡📊

Vitestは V8の仕組みでカバレッジを取れるよ(NodeみたいにV8上で動く環境が必要)(Vitest) カバレッジ用のパッケージもあるよ〜📦(npm)

入れるならこれ👇

npm i -D @vitest/coverage-v8

実行例👇

npx vitest --coverage

12) AI(Copilot/Codex)を使うときの勝ちパターン🤖🏆✨

✅ おすすめプロンプト例(そのまま使ってOK)💬

  • 「この関数のユニットテストをVitestで。境界値異常系も入れて」🧪✨
  • 「テストが書きやすいように、副作用を外に追い出すリファクタ案を出して」🧼🔧
  • 「このテスト、意図が伝わる名前に直して」📝💖

⚠️ 注意ポイント

AIが出したテストは、ときどき

  • 期待値がズレてる
  • 本当は守りたい仕様が抜けてる があるから、最後は人間の目でOK出すのが大事だよ👀✅

13) ミニ課題(やってみよ〜!)🎒✨

課題A:テストをもう2本増やす🧪➕

  • 10%引きのケース🎟️
  • 100円引きのケース💴

課題B:異常系テストを1本書く🚨

  • quantityが0のとき例外になるか
  • unitPriceが負のとき例外になるか

(例外テストはこう書けるよ👇)

import { expect, test } from "vitest";
import { calculateTotal } from "./priceCalculator";

test("quantityが0ならエラー", () => {
expect(() =>
calculateTotal([{ name: "Tea", unitPrice: 300, quantity: 0 }], { type: "none", value: 0 }, 0.1)
).toThrow();
});

課題C:わざとバグらせて、テストが守ってくれるのを見る😈🛡️

  • Math.floorMath.round に変えてみる → テストが落ちたら「守られてる!」ってことだよ🎉

14) 今日のまとめ🎀✨

  • テストしにくい=設計がしんどいサインになりやすい😵‍💫
  • 料金計算みたいなロジックは 純粋関数化するとテストが爆ラク🧡
  • Vitestで まず1本テストを書けたら大勝利🎊😊✨
  • テストは敵じゃなくて、未来の自分の味方〜!🫶🧸✅

次の章(第7章)では、この「テストで守れる状態」を土台にして、安全にリファクタリングしていくよ〜🧼🔁✨