第22章:パラメータ化テスト(ケース増殖の味方)🔁

🎯 この章のゴール
- 「同じ形のテストが増えてきた…😵💫」を、パラメータ化テストでスッキリ整理できるようになる✨
- **境界値(ギリギリの値)**を増やしても、テストが読みやすいまま保てる📚💕
🌱 まずイメージ:こういう時に効く!
たとえばこんなテスト👇
- 点数 → 評価(A/B/C…)
- 料金 → 送料(重さで段階が変わる)
- 税計算 → 端数処理(切り上げ/切り捨て)
こういうのって、入力と期待値が違うだけで、テストの形がほぼ同じになりがちだよね🥺 そこで パラメータ化すると…
- ✅ テスト本体(ロジック)は1回だけ書く
- ✅ ケースは「表(テーブル)」で増やすだけ
- ✅ 境界値を足すのがラクになる💪✨
(Vitestでも test.each / it.each が公式に用意されてるよ🧪) (Vitest)
🧠 どこまでパラメータ化する?(やりすぎ防止🚧)
パラメータ化テストが向くのは👇
✅ 向く
- Arrange(準備)が同じ形
- Assert(検証)が同じ形
- 「入力→出力」が同じパターンで並ぶ
🚫 向かない(分けた方が読みやすい)
- ケースごとに準備が全然違う(DBっぽい準備が混ざる等)
- 1ケースだけ検証観点が違う(例:例外・ログ・副作用だけ別)
- “仕様が違う話”まで同じ表に混ぜちゃう
👉 コツは **「1つのテスト=1つの約束」**のまま、ケースだけ増やすことだよ🫶✨
🧪 Vitestのパラメータ化:まずは王道 test.each 💖
Vitestの test.each は、配列のケースでも、オブジェクトのケースでもOK!
さらにテスト名に %i とか $a みたいに値を埋め込めるのが気持ちいい✨ (Vitest)
🧑💻 手を動かす:境界値が増える「点数→評価」を題材にやってみよ📚💯
① 仕様(今回はこれ!)
- 0〜59 →
F - 60〜69 →
D - 70〜79 →
C - 80〜89 →
B - 90〜100 →
A
(※例外系は前の章でやってる想定だから、今回は正常系の境界を育てる🪴)
② まずは最小でスタート(3ケースだけ)🍼
// src/gradeOf.ts
export type Grade = 'A' | 'B' | 'C' | 'D' | 'F'
export function gradeOf(score: number): Grade {
if (score <= 59) return 'F'
if (score <= 69) return 'D'
if (score <= 79) return 'C'
if (score <= 89) return 'B'
return 'A'
}
テストはこう👇(最初は3つくらいでOKだよ😊)
// tests/gradeOf.test.ts
import { describe, expect, test } from 'vitest'
import { gradeOf } from '../src/gradeOf'
describe('gradeOf', () => {
test.each([
[0, 'F'],
[60, 'D'],
[100, 'A'],
])('score=%i -> %s', (score, expected) => {
expect(gradeOf(score)).toBe(expected)
})
})
📌 ここポイント:%i や %s でテスト名に値を埋め込めるよ(Vitest公式に書いてあるやつ!) (Vitest)
③ ここからが第22章:境界値を “5ケース追加” して強くする💪🧪
境界値って、たとえばこういう「段差の両側」だよね👇
- 59 / 60
- 69 / 70
- 89 / 90
じゃあ表(ケース)を増やすだけでOK✨
import { describe, expect, test } from 'vitest'
import { gradeOf, type Grade } from '../src/gradeOf'
describe('gradeOf', () => {
test.each([
[0, 'F'],
[59, 'F'], // 境界(手前)
[60, 'D'], // 境界(到達)
[69, 'D'], // 境界(手前)
[70, 'C'], // 境界(到達)
[89, 'B'], // 境界(手前)
[90, 'A'], // 境界(到達)
[100, 'A'],
] satisfies ReadonlyArray<readonly [number, Grade]>)(
'score=%i -> %s',
(score, expected) => {
expect(gradeOf(score)).toBe(expected)
},
)
})
💡 satisfies を使う理由(地味に超うれしい)
- ケース表の
expectedを、Grade以外にすると その場で型エラーにしてくれる✅ - 「表に間違いが混ざる事故」減るよ〜!🫶
satisfiesは「型を保ったまま、条件だけ満たしてるかチェック」できる書き方としてよく使われるよ (Zenn)
④ もっと読みやすくする:ケースに “名前” を付ける📝💕
境界値って、失敗した時に「どの境界だっけ?」ってなりがち😇
そこで オブジェクト配列+ $name が便利!
Vitestはテスト名で $a みたいに オブジェクトのプロパティを埋め込みできるよ (Vitest)
import { describe, expect, test } from 'vitest'
import { gradeOf, type Grade } from '../src/gradeOf'
const cases = [
{ name: 'min', score: 0, expected: 'F' },
{ name: 'border 59', score: 59, expected: 'F' },
{ name: 'border 60', score: 60, expected: 'D' },
{ name: 'border 69', score: 69, expected: 'D' },
{ name: 'border 70', score: 70, expected: 'C' },
{ name: 'border 89', score: 89, expected: 'B' },
{ name: 'border 90', score: 90, expected: 'A' },
{ name: 'max', score: 100, expected: 'A' },
] as const satisfies ReadonlyArray<{ name: string; score: number; expected: Grade }>
describe('gradeOf', () => {
test.each(cases)('$name: score=$score -> $expected', ({ score, expected }) => {
expect(gradeOf(score)).toBe(expected)
})
})
これ、落ちた時にログがめっちゃ親切になるよ〜🥹✨
⑤ おまけ:test.for って何?(ちょい発展⭐️)
Vitestには test.each と別に test.for もあるよ!
違いはざっくり👇
each:配列ケースを 引数に展開して渡すfor:配列ケースを そのまま1つの引数として渡す(必要ならTestContextも使いやすい)
公式にも例があるよ🧪 (Vitest)
import { expect, test } from 'vitest'
test.for([
[59, 'F'],
[60, 'D'],
])('score=%i -> %s', ([score, expected]) => {
expect(score < 60 ? 'F' : 'D').toBe(expected)
})
(最初は test.each が分かりやすいから、まずはそっちでOKだよ😊)
🤖 AIの使いどころ(“ケース作り”に全振りが正解🎯)
AIはこう使うと強いよ〜🤖✨(でも採用判断はあなたがするやつ!🫶)
✅ そのまま使えるプロンプト例
- 「この仕様の 境界値 を全部列挙して(段差の両側も)」
- 「不足しがちなケース(見落としやすい境界)を指摘して」
- 「ケース表を
test.each用に、name付きで作って」 - 「このパラメータ化テスト、混ぜるべきじゃない仕様が紛れてないかレビューして」
✅ この章のチェックリスト(合格ライン🎓✨)
- “同じ形のテスト” を
test.eachにまとめられた🧪 - 境界値を 表に足すだけで増やせた🔁
- 失敗した時に「どのケースか」すぐ分かる(
$nameなど)📝 - ケースが増えすぎたら「分ける」判断もできる🚧
🧸 ミニ課題(サクッと提出用💌)
gradeOfのケース表に、あなたが思う境界値を あと5ケース追加してね🧪✨- 追加した理由をコメントで1行ずつ書いてね📝💕(例:「69はDの上限だから」)
次の章(第23章)は「テストの独立性」=順序依存を消す🧵✨ パラメータ化でケース増やした後に、そこを固めるとめちゃ強くなるよ〜💪🥰