問題が発生したときに実際に失敗するテストの書き方

合格するテストを書くのは簡単です。しかし、テスト対象の機能が壊れたときに失敗し、かつそのときだけ失敗するテストを書くことは、多くのエンジニアが認識している以上に難しいことです。
失敗すべき場面で合格するテストは、テストがない場合よりも有害です。根拠のない自信を生み出します。エンジニアはテストスイートが自分たちを守っていると思い込んでコードをマージしますが、実際にはそうではありません。バグはリリースされ、テストは合格し続け、なぜテストで検出できなかったのか誰もすぐには理解できない状況で、本番環境に障害が表面化します。
これは「失敗感度の欠如したテスト」と呼ばれます。よく見られる問題であり、真剣に取り組む価値があります。
テストが失敗しない原因
最も一般的な原因は、間違った対象をアサートすることです。「フォーム送信後にユーザーが成功メッセージを確認できる」というテストで、classにmessageを持つ要素の存在をアサートした場合、メッセージが「成功」であっても「エラー: 問題が発生しました」であっても合格してしまいます。アサーションが広すぎて、関連する失敗を検出できないのです。
サイレントな例外処理も一般的な原因です。テスト内でtry/catchブロックが例外を飲み込んでtrueを返すと、すべての実行パスが合格になります。また、エラーをキャッチして失敗を伝播させる代わりにデフォルト値を返すテストヘルパーは、アプリケーションの動作にかかわらずテストを失敗不能にします。
非同期テストにおける競合状態は、断続的な感度を生み出します。非同期処理の完了前に結果を確認するテストは、タイミングによって失敗を検出したり(チェックが処理完了後に実行された場合)、見逃したり(チェックが処理完了前に実行された場合)します。その結果、テストは確実にではなく断続的にしか失敗せず、エンジニアはその失敗をフレークとして無視するようになります。
ミューテーションテスト: テストが実際に機能することを証明する
ミューテーションテストは、テストの失敗感度を検証するための体系的なアプローチです。このプロセスでは、アプリケーションコードに意図的なバグ(ミューテーション)を導入し、その結果としていずれかのテストが失敗するかどうかを確認します。ミューテーションが存在してもテストが失敗しない場合、そのミューテーションは「生き残った」とみなされ、テストスイートの該当コードパスにカバレッジのギャップがあることを意味します。
高いミューテーションスコア(少なくとも1つのテストによって除去されたミューテーションの割合)は、テストスイートが実際のリグレッションを検出する能力と強い相関があります。ラインカバレッジに加えてミューテーションカバレッジを追跡するチームは、ラインカバレッジだけを追跡するチームよりも欠陥検出率が大幅に向上しています。
失敗を意識したテストの書き方
テストの失敗感度を高めるために最も実践的な習慣は、アサーションを確定する前に「このテストが間違っている場合、どうすればわかるか?」と問うことです。
「テストを注意深く読まないと、誤ったものをアサートしていることに気づけない」という答えが返ってくるなら、そのアサーションは暗黙的すぎます。明示的なアサーション――期待される正確な値、正確なエラーメッセージ、正確なレコード数をアサートすること――は、推論しやすく、偶発的に恒真になりにくいものです。
TestSpriteのAIエージェントは、テストの説明の意図に基づいてアサーションを生成します。これにより、時間的プレッシャーの下でエンジニアが手動で書くよりも具体的なアサーションが生成される傾向があります。エージェントは、意図された動作と緩やかに相関するプロキシではなく、説明に記載された正確なメッセージ、正確な状態変化、正確なナビゲーションをテストします。