エージェントから「テストはすべて通っています」と報告された。GitHub Actionsにも緑色のチェックが付いています。ところが公開URLを開くと、ログイン後の保存ボタンが動きません。

これは、テストが無意味だったという話ではありません。保存ボタンを使う一連の流れが、実行したテストの対象に入っていなかった可能性があります。

テストは「アプリ全体が正しい」と証明する魔法ではありません。決めた条件でコードを動かし、期待した結果になったかを確認する仕組みです。この記事では、エージェントが実行した検査の名前と結果から、何を確認できて、何がまだ未確認なのかを判断できるようにします。

緑のチェックが何を実行したかを見る

Next.jsのプロジェクトでは、次のようなコマンドが使われます。

npm run lint
npm run typecheck
npm test
npm run build

名前はプロジェクトごとに違います。実際のコマンドはpackage.jsonscriptsで確認します。

{
  "scripts": {
    "lint": "eslint .",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "test:e2e": "playwright test",
    "build": "next build"
  }
}

npm testという文字だけを見ても、どのテストを動かしたかは分かりません。scriptsから実体をたどります。

Lint、型チェック、テスト、ビルド

これらは似ていますが、見つけられる問題が違います。

Lint

Lintは、コードの書き方や、問題につながりやすいパターンを検査します。未使用の変数、React Hooksの誤った使い方、プロジェクトで禁止している書き方などを見つけます。

Lintが成功しても、ボタンを押した結果やAPIの返答を確認したことにはなりません。

型チェック

TypeScriptの型チェックは、関数へ渡す値や返り値など、型の不一致を探します。

Type 'string' is not assignable to type 'number'.

型の見方は「AI開発で困らないためのTypeScript」で扱っています。型チェックが通っても、外部APIから本当に期待したJSONが届くことや、権限設定が正しいことまでは保証されません。

テスト

テストコードは、用意した入力や操作に対する結果を確認します。

expect(calculateTotal([100, 200])).toBe(300)

何を入力し、何を期待するかを書いていなければ、そのケースは確認されません。

ビルド

next buildは、本番用のアプリを作れるか確認します。import先がない、サーバーとブラウザの境界が合わないなど、開発中には見えなかった問題が出ることがあります。

ビルド成功は、成果物を作れたという証拠です。公開URLで実際のデータベースや外部APIまで動いた証拠ではありません。

テストには確認する大きさがある

Next.jsの公式ガイドでは、単体、コンポーネント、結合、E2Eなどのテストが紹介されています。それぞれ役割が違います。

関数を確認する単体テストから、部品、APIやDBとの結合、利用者の操作全体を確認するE2Eまで、対象範囲が広がる入れ子の図

単体テスト

一つの関数など、小さな単位を切り出して確認します。

function calculateTax(price: number) {
  return Math.floor(price * 0.1)
}

expect(calculateTax(1000)).toBe(100)

高速で、失敗した場所を特定しやすい反面、データベースや画面と組み合わせた動作は確認しません。

コンポーネントテスト

Reactコンポーネントを表示し、propsやクリックへの反応を確認します。

render(<SaveButton disabled={false} />)
expect(screen.getByRole("button", { name: "保存" })).toBeEnabled()

ボタンの表示や画面内の動きは確認できますが、本物のAPIを呼ばずに置き換えている場合があります。

結合テスト

複数の処理を組み合わせて確認します。APIが入力を検証し、データベースへ保存し、正しいレスポンスを返すところまでを対象にすることがあります。

単体では正しい処理でも、項目名やデータ形式の食い違いは、組み合わせたときに見つかります。

E2Eテスト

End-to-Endテストは、ブラウザを操作し、利用者の流れを端から端まで確認します。

  1. ログインする
  2. 投稿画面を開く
  3. タイトルを入力する
  4. 保存する
  5. 再読み込みしても投稿が残っていることを確認する

実際の利用に近い一方、実行に時間がかかり、環境や外部サービスの状態にも影響されます。すべてをE2Eにすればよいわけではありません。

正常系だけでは足りない

入力が正しく、通信も成功するケースを正常系と呼びます。まず必要ですが、実際の不具合は失敗時にも起きます。

投稿フォームなら、次のケースがあります。

  • タイトルが空
  • 文字数が上限を超える
  • 同じボタンを連続で押す
  • ログインの期限が切れる
  • APIが500を返す
  • 通信が途中で切れる
  • 保存後の画面更新だけ失敗する

どのケースを確認するかは、機能の重要度と、失敗したときの影響で決めます。課金や削除など、元に戻しにくい操作は、成功より失敗時の設計に時間を使います。

回帰テストは直した不具合を残す

不具合を直すときは、できれば先にその不具合を再現するテストを追加します。

  1. 不具合を再現するテストを書く
  2. テストが失敗することを確認する
  3. コードを修正する
  4. 同じテストが成功することを確認する

このテストは、あとで同じ不具合が戻ってきたときに検知できます。これを回帰テストと呼びます。

エージェントが「再現テストを追加しました」と報告したら、修正前には本当に失敗したのかを確認します。最初から成功するテストでは、対象の不具合を捉えていない可能性があります。

MockとFixture

テストでは、外部API、時刻、データベースなどを本物の代わりへ置き換えることがあります。これをmockと呼びます。

たとえば、本物の決済APIを、テスト用の決まった返答へ置き換えます。

mockを使うと、高速で安定したテストができます。料金も発生しません。ただし、本物のAPIの仕様が変わってもmockは古い返答を返し続けます。

fixtureは、テストで使う固定データです。

{
  "id": "user-test-1",
  "name": "テスト利用者"
}

mockやfixtureが実際のデータと違えば、テストだけ成功します。外部サービスとの接続は、別の結合テストや確認環境でも確かめます。

テスト用データベースを分ける

テストがデータを作成、変更、削除する場合、本番データベースへ接続してはいけません。

  • テスト: テスト用DB
  • Preview: 開発用またはPreview用DB
  • Production: 本番DB

テスト開始前に初期状態を作り、終了後に後片付けします。テスト同士が同じデータを使うと、実行順によって成功したり失敗したりすることがあります。

マイグレーションのテストでは、空のDBだけでなく、既存データが入った状態から更新できるかも確認します。マイグレーションそのものは「AI開発で困らないためのデータベース入門」で説明しています。

失敗メッセージを読む

テストが失敗したら、最初に見るのは次の三つです。

  • どのテストが失敗したか
  • 何を期待していたか
  • 実際は何だったか
Expected: 201
Received: 403

この例では、作成成功の201を期待したのに、権限拒否の403が返っています。認証情報がテストへ渡っていないのか、権限判定が変わったのかを調べます。

エラーの最後だけでなく、最初に失敗したテストを確認します。一つ目の失敗によって後続のテストが連鎖的に失敗することがあるからです。

不安定に成功と失敗を繰り返すテストをflaky testと呼びます。単に再実行して緑にするのではなく、時刻、通信、共有データ、待機条件のどこに不安定さがあるかを調べます。

「確認済み」を証拠ごとに分ける

エージェントの報告では、次を分けてもらいます。

確認 分かること まだ分からないこと
Lint コード規約や一部の危険な書き方 実際の機能動作
型チェック 型の整合性 外部データや画面操作
単体テスト 小さな処理の結果 処理同士の接続
結合テスト 複数処理の接続 本番環境固有の設定
E2E 想定した利用者操作 網羅していない別の操作
ビルド 本番成果物を作れる 公開後の接続先
公開URL確認 その環境の実際の動作 別権限・別端末など未実施の条件

シリーズ内のデプロイ記事でも説明したとおり、CIの成功と公開URLでの確認は別の証拠です。

エージェントが追加したテストを確認する

  • 変更した機能のどの動作を確認しているか
  • 正常系だけでなく、重要な失敗系があるか
  • 修正前に不具合を再現できたか
  • mockと本物のサービスを区別しているか
  • テストデータが本番へ接続していないか
  • テスト同士がデータを共有していないか
  • flakyなテストを再実行だけで済ませていないか
  • Lint、型チェック、テスト、ビルドを別々に報告しているか
  • 認証が必要なE2Eを実行できたか
  • 公開URLでの確認結果をCI結果と分けているか

エージェントには、次のように頼めます。

今回の変更について、Lint、型チェック、単体・結合・E2E、ビルドのうち何を実行したか分けてください。 各確認で分かったことと、未確認のことを報告してください。

この不具合を再現するテストを先に追加し、修正前に失敗することを確認してください。 修正後は同じテストが成功した証拠を示してください。

テストで使っているmock、fixture、データベースを一覧にしてください。 本番の認証情報や本番データへ接続していないことを確認してください。

テスト結果を読むときは、「通ったか」より「何を試したか」を先に見ます。実行した検査と、まだ人が確認する必要のある操作を分けられれば、緑のチェックを過信せずに使えます。