開発プロセス●●○○○

テスト

コードが意図通り動くことを検証する営み。自動化して初めて資産になる。

概要

テストとは、コードが意図通りに動くことを検証する営みです。手で動かして確認するのもテストですが、ソフトウェア開発で「テスト」と言えば、多くの場合は検証手順をコードとして書き、何度でも自動実行できるようにした「自動テスト」を指します。assert add(2, 3) == 5 のように「この入力ならこの結果になるはず」という期待を書き並べ、テストランナーが一括で照合します。

自動テストの真価は、バグを見つけること以上に「安心してコードを変更できる」ことにあります。既存の振る舞いがテストとして固定されていれば、変更で何かを壊した瞬間にテストが赤くなって教えてくれます。リファクタリングも、CI/CDによる自動デプロイも、この安全網があって初めて成立します。テストは書いた瞬間よりも、コードが変更され続ける限り効き続けるという意味で「資産」なのです。

なぜ生まれたか

ソフトウェアは完成品ではなく、変更され続けるものです。手動確認だけに頼っていた時代の最大の問題は「リグレッション(デグレ)」——ある箇所の修正が、以前は動いていた別の箇所を壊す現象——でした。変更のたびに全機能を手で確認し直すのは現実的でなく、確認範囲を絞れば見逃しが生まれる。コードベースが育つほど変更が怖くなり、「動いているコードには触るな」という保守的な文化に行き着いてしまいます。

2000年前後、JUnitに代表されるテスティングフレームワークとテスト駆動開発(TDD)の登場で、「検証をコードとして書き、変更のたびに全部を数秒〜数分で再実行する」やり方が広まりました。検証コストがほぼゼロになれば、リグレッションはコミットの直後に発覚します。テストの自動化は、アジャイル開発や継続的デリバリーといった「頻繁に変更を出荷する」現代のスタイルを支える前提条件になりました。

詳細

テストピラミッド: 3つの階層

自動テストは検証の範囲によって階層に分かれます。関数やクラス単位で検証する「ユニットテスト」、複数の部品(アプリとRDBMSAPI同士など)のつながりを見る「統合テスト」、ブラウザ操作などでユーザーの操作を通しで確認する「E2E(End-to-End)テスト」です。下の層ほど速く・安く・原因の特定が容易で、上の層ほど遅く・壊れやすい代わりに本物に近い検証になります。

この性質から「下を厚く、上を薄く」構成するのが定石で、その形からテストピラミッドと呼ばれます。ユニットテストを大量に、統合テストを適量、E2Eテストは主要な導線に絞って少数——という配分です。

E2Eテスト少数・遅い・本物に近い統合テスト適量・部品のつながりを検証ユニットテスト大量・速い・原因特定が容易上ほど本物に近い(忠実度)下ほど数が多い・速い・安い
テストピラミッド: 段の幅がテストの数を表す。下ほど多く・速く、上ほど少なく・本物に近い

逆にE2Eばかりに頼ると、実行に時間がかかり、UIの些細な変更で大量に壊れ、失敗しても原因箇所が分からない「逆ピラミッド」の苦しみを味わうことになります。

テストが開発フローに組み込まれるまで

現代のチーム開発では、テストは個人の habit ではなくプロセスに組み込まれています。Gitでブランチを切って変更し、プルリクエストを出すとCI/CDパイプラインが自動でテストを実行し、全部通らなければマージできない——という関門方式が標準です。

コードとテストを書くプルリクエスト作成CIが全テストを自動実行成功レビューマージ失敗修正して再プッシュ再びCIへデプロイ前の最終検証でも同じテストが走る
CIを関門にした開発フロー: テストが失敗すれば修正ループに戻り、全部通るまでマージできない

この仕組みの要点は「テストが通らないコードは本流に入れない」を機械的に強制することです。人間の注意力に頼らないからこそ、チームの規模が大きくなっても品質の下限が保たれます。

良いテストの条件とテストダブル

テストコードにも品質があります。良いテストは、速い(頻繁に回せる)、独立している(実行順序や他のテストに依存しない)、決定的である(同じコードなら必ず同じ結果になる)、そして「何を検証しているか」が読んで分かる、という条件を満たします。特に厄介なのが、たまに理由なく失敗する「flaky(不安定)なテスト」です。時刻・乱数・非同期処理の待ち方・外部サービスへの依存が典型的な原因で、放置するとチームが赤いCIを無視するようになり、テスト全体の信頼が崩れます。

外部依存を切り離す道具が「テストダブル」です。本物のAPIやDBの代わりに、決まった応答を返す偽物(スタブ)や、呼ばれ方を記録する偽物(モック)を差し込むことで、テストを速く決定的にできます。ただしモックを使いすぎると「モックの動きを検証しているだけで、本物との結合は検証していない」状態に陥ります。ユニットテストはモックで速く回し、本物同士の結合は統合テストで別途押さえる、という役割分担が現実的です。

何をどこまでテストするか

テストカバレッジ(テストが通ったコードの割合)は便利な指標ですが、目的化すると「アサーションのない実行するだけのテスト」を量産する誘惑を生みます。数字より大切なのは、壊れると困る振る舞い——料金計算、認証認可の境界、データを失う可能性のある処理——から優先的に固めることです。また、実装の内部構造ではなく外から見た振る舞いをテストするのが原則です。内部に密結合したテストはリファクタリングのたびに壊れ、「変更を守る」はずのテストが「変更を妨げる」存在になってしまいます。

テストを書くタイミングにも流儀があります。実装より先にテストを書くTDDは設計を駆動する手法として有名ですが、必ずしも全員がTDDで書く必要はありません。重要なのは、コードとテストが同じプルリクエストで一緒に育ち、バグ修正の際には「そのバグを再現するテスト」を先に書いて再発を封じることです。