なぜ要るか
6章までで、ゲームには仕組みが増えました。データ、セーブ、記録、判定。触る場所が増えると、直したつもりで別の場所が壊れます。
6-1でこう書きました。
powerを書き忘れた札がある
→ 黙って undefined になり、比較が全部 false になります。
気づきにくい壊れ方です。
気づきにくい壊れ方を、機械に見張らせます。 それがテストです。
テストというのは、自分のプログラムの一部を自動で呼び出して、期待した答えが返るかを確かめる小さなプログラムです。人が画面を開いて目で確かめる代わりに、機械が一瞬で全部確かめてくれます。
そして幸運なことに、テストはAIが得意な作業です。 判断ではなく手順だからです。
この術で使う Vitest は、書いたテストをまとめて実行して、通った件数と落ちた件数を出してくれる道具です。テストそのものを書くのはAIに任せられます。
使う道具: Vitest
AIプロンプト
この術の作業をAIへ任せるときは、これをそのまま貼ってください。
index.html の takerOf のテストを vitest で書いて。
確かめること:
- 合計が多い方が取る(自分側・相手側の両方向)
- 誰も置いていない的は、どちらのものでもない
- 同点で、あとに置いた方が取る(自分が後・相手が後の両方向)
- 同点で同じ手番なら、どちらのものでもない
- DECK に無いidを渡したときに落ちること
期待値は DECK から導出して、札のidを直書きしないで。
書いたあと、わざと takerOf を壊して落ちることを確認して、元に戻して。
「確かめること」の中身は、自分の作品で壊れたら困る判定に置き換えてください。
テストは「壊れたら気づく仕掛け」です
先に誤解を解きます。テストは正しさを証明するものではありません。壊れたことに気づくための仕掛けです。
だから目標はこうなります。
| 目標 | 中身 |
|---|---|
| ○ | 前に動いていたものが動かなくなったら、すぐ分かる |
| × | バグが1つもないことを証明する |
全部にテストを書く必要はありません。 壊れたら困る場所にだけ書きます。
テストは3種類あります
ここは知識の話です。言葉を知らないとAIに頼めないので、先に押さえます。
テストは「どこまでを通して確かめるか」で3つに分かれます。
| 種類 | 何を確かめるか | 例 | 速さ | 壊れやすさ |
|---|---|---|---|---|
| 単体(ユニット) | 部品1つを単独で | takerOf が正しい取り手を返す |
速い(1件0.001秒) | 壊れにくい |
| 結合(インテグレーション) | 部品をつないで | セーブして読み直すと同じ | 中 | 中 |
| E2E(エンドツーエンド) | 利用者と同じ経路で端から端まで | ブラウザで札を押して勝敗が出る | 遅い(1件数秒) | 壊れやすい |
E2E は「端から端まで」の意味です。 中身を直接呼ばず、人がやるのと同じ操作をさせます。だから一番信頼できて、一番遅くて、一番壊れます。
数の比は下が多く、上が少ない
これは「テストピラミッド」と呼ばれる形です。僕のCNPトレカアプリで数えると、実際にそうなっています。
単体 198本 ← 一番多い。実装ファイルの隣に置く
結合 22本
E2E 13本 ← 一番少ない
逆にしてはいけません。 E2Eを増やすと、実行に何分もかかり、たまに落ちるようになり、だんだん結果を見なくなっていきます。
理由は単純で、E2Eは通る/落ちるしか分からないからです。落ちても「どこが悪いか」は教えてくれません。単体テストなら、落ちた1件が壊れた場所を指しています。
この講座のサイトの場合
小さい側の実例も出します。いまこのサイトのテストはこうなっています。
単体 35件 会員の処理(暗号化、登録、認証、退会)
データ 16件 記事と道具箱の整合(idの重複、必須項目、図解の実在)
結合 24件 HTTPの経路を通して画面を組み立てる
E2E 0件
合計 75件 テストの実行 約2秒(準備を含めた全体で約9秒)
E2Eはまだ1件も書いていません。 それで困っていません。
規模が小さいうちは、単体と結合で足ります。 E2Eを足すのは、画面の操作が複雑になってからです。
単体テストは外を触りません
単体テストが速い理由は、外部を一切使わないからです。
| 使わないもの | 代わりにすること |
|---|---|
| データベース | メモリ上の入れ物で代用する |
| 通信 | 呼ばれたことだけ記録する偽物を渡す |
| 現在時刻 | 固定の時刻を渡す |
| 乱数 | 決まった値を返すものを渡す |
この「偽物を渡す」仕組みをモックやスタブと呼びます。呼び方の違いは気にしなくて構いません。大事なのは、外部を差し替えられる形に作ってあるかです。
差し替えられる形とは、外部を引数で受け取るという意味です。
// 差し替えられない: 中で直接時刻を取っている
function save() { const at = new Date() }
// 差し替えられる: 外から渡す
function save(now) { const at = now }
テストが書きにくいコードは、たいてい外部を中で直接呼んでいます。 テストを書こうとすると、この設計の問題が先に見つかります。それもテストの効果の1つです。
覚えておくと頼みやすい言葉
AIに頼むとき、この言葉を使えると通りが良くなります。
| 言葉 | 意味 |
|---|---|
| テストランナー | テストを実行する道具(この講座では Vitest) |
| アサーション | 「こうなっているはず」を書く行(expect(...)) |
| フィクスチャ | テスト用に用意する前提データ |
| 回帰テスト | 前に直した不具合が再発していないかを見るテスト |
| カバレッジ | コードのどれだけをテストが通ったかの割合 |
| フレーキー | たまに落ちるテスト。一番厄介な状態 |
「回帰テスト」が実務では一番出てきます。 7-3で「見落としたものを機械へ移す」と書きますが、あれは全部この回帰テストです。
なおカバレッジは目標にしないでください。 100%にしても壊れは残ります。数字を上げるためのテストは、意味のない見張りになります。
どこに書くか
判断の順番はこれです。上から順に、費用対効果が高い順です。
| 優先 | 対象 | 例 |
|---|---|---|
| 1 | ルールの判定 | 勝敗、条件、計算 |
| 2 | データの整合 | idの重複、必須項目の欠け |
| 3 | 保存と復元 | セーブして読み直すと同じか |
| 4 | 画面の見た目 | 最後。変わりやすいので壊れやすい |
1番目から書いてください。 勝敗の判定は、間違うと全部が狂います。しかも入力と出力が明確なので、テストが書きやすいです。
逆に4番目は後回しです。画面はよく変わるので、テストの方が先に壊れます。
僕が今日書いたテストの話
抽象的な話より実例が早いので、この講座のサイトで今日起きたことを書きます。
記事を書いていて、こういう文を入れました。
だから**「同点の的」の扱いを決める場所がありません。**
これは強調が効きません。 日本語だと ** の直後が 「 のとき、Markdownが強調として扱わないルールがあるためです。結果、画面に ** がそのまま出ます。
そして僕は、それを6箇所で見落としていました。 うち4箇所は、すでに公開している記事の中にありました。
目で読んでも気づけないので、機械に見せることにしました。
it('公開済みの術に、強調が崩れて ** が残っていない', () => {
const broken = WAZA
.filter((w) => w.status === 'published' && w.html.includes('**'))
.map((w) => `${w.id}: ...`)
expect(broken).toEqual([])
})
書き方を先に説明します。it('説明', () => { ... }) が1件のテストで、最初の文字列がそのテストの名前です。落ちたときにこの名前が出ます。expect(A).toEqual(B) は「AはBのはず」という宣言で、違っていればそこで落ちます。ここでは公開済みの記事から崩れが残っているものだけを集め、その一覧が空であることを確かめています。
やっていることは1つです。組み立てたHTMLに ** が残っていたら落とす。
これで以降は、書いた時点で気づけます。目で見て確認するのをやめました。
テストが本当に落ちるか確かめる
ここを飛ばす人が多いです。通っているテストは、何もチェックしていないかもしれません。
だからわざと壊して、落ちることを確認します。
僕は上のテストを入れたあと、記事を1行わざと壊しました。
Tests 1 failed | 13 passed (14)
AssertionError: expected [ Array(1) ] to deeply equal []
+ "6-1: この形だと、的の数がコードに散らばります。だから**「増えるもの」..."
落ちました。 そして元に戻すと通りました。
Tests 14 passed (14)
これで「このテストは本当に見ている」と言えます。この確認をしていないテストは、あってもゼロと同じです。
AIへの頼み方
テストを書かせるとき、そのまま「テストを書いて」と言うと薄いものが出ます。
決めて渡すのはこの3つです。
claude -p "index.html の takerOf のテストを vitest で書いて。
確かめること:
- 合計が多い方が取る(自分側・相手側の両方向)
- 誰も置いていない的は、どちらのものでもない
- 同点で、あとに置いた方が取る(自分が後・相手が後の両方向)
- 同点で同じ手番なら、どちらのものでもない
- DECK に無いidを渡したときに落ちること
期待値は DECK から導出して、札のidを直書きしないで。
書いたあと、わざと takerOf を壊して落ちることを確認して、元に戻して。"
効いているのは後半2行です。
| 指定 | なぜ効くか |
|---|---|
| 期待値をデータから導出 | 札を増やしたときにテストが勝手に付いてくる |
| わざと壊して落ちるか確認 | 通るだけのテストを防ぐ |
「わざと壊して確認して」は必ず入れてください。 これを言わないと、AIは通るテストを書いて満足します。
前提を毎回書かせない
テストが増えると、同じ準備を何度も書くようになります。
// 毎回これを書いていると、テストが読めなくなる
const side = { mine: [{ card: 'c3', turn: 0 }, { card: 'c1', turn: 2 }],
theirs: [{ card: 'c4', turn: 1 }] }
だから前提を作る道具を1つ用意します。
export function makeSide(mine: [string, number][], theirs: [string, number][]) {
const put = (a: [string, number][]) => a.map(([card, turn]) => ({ card, turn }))
return { mine: put(mine), theirs: put(theirs) }
}
it('同点なら、あとに置いた方が取る', () => {
expect(takerOf(makeSide([['c3', 0], ['c1', 2]], [['c4', 1]]))).toBe('me')
})
makeSide は、[札のid, 何手目] の並びを渡すと的1つ分の盤面データに組み立ててくれる道具です。おかげでテスト本文は「自分が3を0手目と1を2手目、相手が4を1手目」という中身だけを書けば済みます。
テスト本文が1行で読めるようになります。 差分だけを引数で渡す形にするのが要点です。
これはAIに任せる観点でも効きます。準備の書き方が1つに決まっていると、出てくるテストが揃います。
やってみる
判定にテストを書かせる
上の頼み方をそのまま使ってください。
落ちることを確認する
npm testnpm testは、用意したテストを全部まとめて実行するコマンドです。何件通って何件落ちたかが数行で出ます。通ったら、わざと壊してください。
takerOfの>を<にするだけで十分です。落ちなければ、テストが見ていません。データの整合も見張らせる
6-2で作った
checkDeckを、テストからも呼びます。it('札のデータに間違いがない', () => { expect(checkDeck()).toEqual([]) })起動時のチェックとテストで同じ関数を使うのが要点です。 2つ書くと、片方だけ直る日が来ます。
保存と復元を1往復で確かめる
it('保存して読み直すと同じ状態になる', () => { const before = snapshot() save(); reset(); load() expect(snapshot()).toEqual(before) })snapshot()は、いまの状態をまとめて1つのデータとして取り出す関数です。保存して、いったん初期化して、読み直して、元と同じに戻っているかを見ています。1往復させるのがコツです。 保存だけ、読込だけを別々に試すより、はるかに壊れが見つかります。
保存する
git add . git commit -m "判定・データ・セーブにテストを入れた" git push
つまずきどころ
テストが多すぎて実行が遅い
いまの規模なら数秒で終わります。この講座のサイトは75件でテスト部分が約2秒(準備を含めても約9秒)です。遅いと感じたら、外部と通信しているテストが混ざっています。
画面のテストがすぐ壊れる
画面は変わるものなので、正常です。文字やレイアウトを細かく検査しないでください。「押せる札が3枚ある」のような、意味の単位で見ます。
AIが書いたテストの意味が分からない
分からないテストは消してよいです。 意味の分からない見張りは、落ちたときに直せません。「1テスト1つの意図」に書き直させてください。
乱数が入っていて結果が変わる
6-6で書いた乱数の固定を使ってください。テストで Math.random をそのまま使うと、たまに落ちるテストになります。 たまに落ちるテストは、やがて無視されます。
時刻に依存して落ちる
同じ理由です。時刻は外から渡す形にして、テストでは固定値を入れます。
テストを書く時間がない
1つだけ書いてください。 勝敗の判定です。そこが壊れると全部狂うので、1つでも効果があります。
Windows の場合
npm test は同じです。
次の一歩
判定のテストを1つ書いて、わざと壊して落ちることを確認してください。
大事なのは後半です。落ちるところを1回見ておくと、テストへの信頼が変わります。 見張りが本当に見張っていると分かるからです。
壊れたら気づける形になりました。次は、その確認を人が実行しなくても回るようにします!