テストの層とリファクタリング — Vibe Coding 時代の守り方

リファクタリング & プロダクト化 の「テストで今の動きを固定する」を、どのテストで・何を・どの順でまで具体化したページです。 合言葉は「判断は単体テストで、つながりは E2E で」。そして「同じ動きかどうかは、推測せず走らせて確かめる」。

AI にコードを書かせると、速く大量に変わります。人間が全部の行を読んで「壊れていない」と判断するのは無理です。だから壊れたら機械が赤くしてくれる状態を先に作り、その中で直します。


1. テストの層(何で何を守るか)

層 何を守るか 速さ 例
🧪 単体テスト 1つの判断(計算・並べ替え・検証・整形)が正しいこと 一瞬。何百本でも毎回回せる node:test / vitest で純粋関数を呼ぶ
🔗 結合テスト(ゴールデンテスト) 部品を組み合わせた出力が変わっていないこと 速い CLI を実行し、出力を git に置いた「正解ファイル」と比べる
🖥 E2E(API をまねる) 画面の操作 → 表示の流れがつながっていること 遅め(ブラウザを起動) Playwright で画面を操作。サーバーの返事は偽物を返す
🌐 E2E(本物につなぐ) 本物のサーバー・DB・外部サービスまで通しで動くこと 遅い・不安定になりやすい 起動済みの開発サーバーに Playwright でつなぐ
👀 起動して手で確かめる 自動化していない部分。「本当に起動するか」 人間の時間を使う yarn dev で開いて、変えた道筋を実際に通る

下の層ほど速くて安く、たくさん書ける。上の層ほど本物に近いが、遅くて壊れやすい。だから「下を厚く、上は大事な道筋だけ」が基本形です(いわゆるテストピラミッド)。


2. 判断は単体で、つながりは E2E で

どちらか片方だけでは守れません。

  • E2E で判断を網羅しようとしない
    • 「空の入力」「境界の値」「壊れたデータ」を全部ブラウザ経由で試すと、遅すぎて誰も回さなくなる。
    • 判断は純粋関数に切り出して、単体テストで「際」のケースを全部押さえる。
  • 単体テストだけで「つながっている」とは言えない
    • ルールを関数に切り出して30ケースのテストを書いても、呼び出し側がその関数を使っていなければ、画面は壊れたまま全部緑になる。
    • 「ボタンを押したら、その関数を通って、結果が画面に出る」は E2E か、起動しての確認でしか分からない。

🧭 迷ったら:「この失敗は、値が間違う失敗か、つながりが切れる失敗か?」 値の間違い → 単体テスト。つながりの切れ目 → E2E。


3. 単体テスト — 判断を純粋関数に切り出して守る

  • 判断と入出力を分ける:ファイル読み書き・通信・時刻は呼び出し側に残し、判断のルールだけを別ファイルの純粋関数にする。起動しないと届かないルールは、結局テストされません。
  • 外部のものは引数で渡す:now()(現在時刻)やファイルのパスを中で直接呼ばず、引数で受け取る。テストでは偽物を渡せます。
  • 「際」のケースを書く(AI に頼むときもこの一覧を渡す)

    種類 例
    普通の入力 期待どおりの値
    空 空文字・空配列・空オブジェクト
    null / undefined 値が無い
    境界 最小・最大・1つずれ
    不正な入力 型が違う・壊れたデータ
    失敗 例外が投げられるべき場面
    起きてはいけないこと 「これは通してはいけない」
    過去のバグ 一度直したバグの再発防止
  • 静かに間違うものから先に:例外を投げる関数は、壊れたら自分で知らせてくれます。怖いのはもっともらしい間違った値を返す関数(1つずれた番号、日付のずれ、甘すぎる入力チェック)。こちらを先にテストします。

4. 結合テスト — 出力を「正解ファイル」と比べる

CLI やプログラムの出力のように、組み合わせた結果が文字やファイルで出るものはゴールデンテストが向いています。

  • 正しい出力をファイルとして git に置く(正解ファイル)。
  • CI で実際の出力と比べ、1文字でも違えば赤。
  • 出力をわざと変えたときだけ、正解ファイルを更新する。更新の差分は PR で人間が読む。

リファクタリング(動きを変えない変更)では、正解ファイルが1行も変わらないのが合格条件です。


5. E2E — 大事な道筋だけを、本物のブラウザで

道具は Playwright

Web アプリなら Playwright が定番です。本物のブラウザを動かし、クリック・入力・表示の確認を自動で行います。AI もよく知っている道具なので、書かせやすいです。

二本立てにする(MulmoClaude の例)

MulmoClaude では、E2E を目的の違う2種類に分けています(説明文書)。

  e2e/(API をまねる) e2e-live/(本物につなぐ)
サーバー テストが自動で起動 起動済みの開発サーバーを使う
サーバーの返事 全部を偽物で返す(mockAllApis(page) 必須) 偽物なし
向いているもの 画面の流れ(開く・押す・表示される) エージェントや外部連携まで通した動き
いつ回すか PR ごと(分割して並列) PR ごと(重い場面は省く)+ 毎日の定期実行
  • API をまねる E2E は、返事が毎回同じなので安定して速い。PR のたびに回せます。偽物の返事をまとめた関数(e2e/fixtures/api.ts)を最初に作り、全テストがそれを使うようにします。
  • 本物につなぐ E2E は、偽物では分からない「本当につながっているか」を確かめる代わりに、遅くて不安定になりがちです。定期実行のワークフローでは、AI の応答が要る場面を飛ばして、それ以外を毎回回しています。

壊れにくい E2E を書くコツ

  • 要素は data-testid で取る:クラス名や画面上の位置で取ると、見た目を変えただけで壊れます(ロケーターの公式ガイド)。
  • 固定時間の sleep で待たない:「返事が届くまで待つ」(waitForResponse)や「表示されるまで待つ」で待つ。時間で待つと、遅い日に落ち、速い日には無駄に待ちます。
  • 大事な道筋に絞る:ログイン、保存、決済のような「壊れたら困る流れ」を、うまくいく場合と、断られる場合の両方で。細かい分岐は単体テストに任せます。
  • 定期実行には受け取り手を決める:夜中に赤くなっても誰にも届かないジョブは、そのうち全員が無視します。失敗したら issue を自動で作る、担当者に通知する、などを先に決めます。

6. 層を使ったリファクタリングの手順

初心者がつまずくのは「テストを書いてから直せと言われても、絡まっていてテストが書けない」ところです。そのときは外側から固めます。

  1. 🧭 変える場所が「みんなの通り道」か見る
    • すべてのリクエストやすべての画面が通る場所(入口・共通の処理・よく呼ばれる関数)なら、テストが緑でも必ずアプリを起動して確かめる(手順 7)。
  2. 🖥 外側を E2E(かゴールデンテスト)で固定する
    • 中身が絡まっていても、「この操作をするとこう表示される」は外から固定できます。今の動きをそのまま記録するテストです(特性テスト)。
    • 今の動きにバグがあっても、まずはそのまま固定。直すのはリファクタリングとは別の変更です。
  3. 🔴 テストをわざと壊して、赤くなるか確かめる
    • 条件を反対にする、一行消すなどして、テストが赤くなるのを見てから元に戻します。壊しても緑のままなら、そのテストは別のものを見ています。
  4. ✂️ 判断を1つだけ切り出す
    • 1回の変更で1つ。切り出した純粋関数には、すぐに単体テスト(§3 の「際」のケース)を付けます。
  5. ⚖️ 「同じ動き」は走らせて確かめる
    • 古いコードを捨てずに残し、新しいコードと同じ入力をたくさん流して結果を比べます。「読んだ限り同じはず」は証拠になりません。
    • 比べ終わって古いコードを消す前に、「どんな入力が大事だったか」の一覧と「常に成り立つべき性質」(並び順が保たれる、合計が変わらない など)を、残すテストに書き写します。
  6. 🔌 呼び出し側を壊して確かめる
    • 切り出した関数をわざと使わないように戻してみて、何かが赤くなるか。何も赤くならないなら、その関数は「誰も呼んでいない関数」としてテストされているだけです。E2E か結合テストを足します。
  7. 👀 起動して、変えた道筋を通る
    • うまくいく場合と、断られる場合の両方。確かめられなかったことは「確かめていない」と PR に書きます。黙っていると「全部確かめた」と読まれます。
  8. 🙅 示せないなら、やらない
    • 動きが同じだと示せない切り出しは、見送るのも正しい選択です。「やると何が良くなるか・何が危ないか・先に何を確かめる必要があるか」を issue か PR に書き残せば、次の人がそこから始められます。

⚠️ 目的は短くすることではありません。関数が短くなっても動きが危うくなるなら悪い取引、短くならなくても壊れたら赤くなる判断が1つ増えたなら良い取引です。


7. AI に頼むときの指示文(例)

このファイルをリファクタリングしたい。動きは変えない。
1. まず今の動きを固定するテストを書いて。画面の流れは Playwright の E2E(API は偽物を返す)、
   判断の部分は単体テスト。
2. 書いたテストは、対象のコードをわざと壊して赤くなることを確認して、その結果を見せて。
3. 判断を1つずつ純粋関数として別ファイルに切り出して。1回の変更で1つ。
   切り出した関数には、空・null・境界・不正な入力・失敗のケースを含む単体テストを付けて。
4. 切り出した関数を呼ばないように戻すと、どのテストが赤くなるかを見せて。
5. 動きが同じだと示せないところは切り出さずに、理由を書いて報告して。

AI が「テストは全部通りました」と言ったら、「壊したときに赤くなったか」を聞き返してください。通るだけのテストは、AI が一番よく作る失敗です。


8. チェックリスト

  • 判断は純粋関数にあり、単体テストで「際」のケースまで押さえている
  • CLI やファイル出力は、正解ファイルとの比較(ゴールデンテスト)で守っている
  • 大事な道筋は E2E で、うまくいく場合と断られる場合の両方がある
  • E2E は data-testid で要素を取り、固定時間で待っていない
  • PR ごとの E2E はサーバーの返事を偽物にして安定させ、本物につなぐ E2E は定期実行で回している
  • 定期実行が失敗したときの受け取り手が決まっている
  • 新しいテストは、わざと壊して赤くなるのを確認した
  • 切り出した関数は、呼び出し側を壊すと何かが赤くなる
  • みんなの通り道を変えたときは、アプリを起動して確かめた。確かめていないことは PR に書いた

合わせて読む