開発プロセス●●○○○

テスト

Testing ・ てすと

コードが意図どおり動くことを検証する営み。自動化して何度も回せるようにして初めて資産になる。

✦ 30秒で試す — 航海テストが全部緑なのに、購入ボタンが次へ進まないユニットテスト40本で捕まるバグは、3つのうちいくつ?予想してみる

概要

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

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

なぜ生まれたか

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

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

詳細

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

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

この性質から「下を厚く、上を薄く」構成するのが定石で、その形からテストピラミッドと呼ばれます(Mike Cohn が2009年の著書『Succeeding with Agile』で広めた図です)。ユニットテストを大量に、統合テストを適量、E2Eテストは主要な導線に絞って少数——という配分です。

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

逆にE2Eばかりに頼ると、実行に時間がかかり、UIの些細な変更で大量に壊れ、失敗しても原因箇所が分からない「逆ピラミッド」(アイスクリームコーンとも呼ばれます)の苦しみを味わうことになります。E2E テストが落ちたとき分かるのは「購入の流れのどこかが壊れた」ことだけで、画面・API・データベース・計算のどこが原因かは、改めて調べなければなりません。ユニットテストが落ちれば、落ちたテストの名前がそのまま「どの関数の、どの条件が壊れたか」を教えてくれます。

ただしピラミッドは「上の段を0にしてよい」という意味ではありません。部品同士のつなぎ目の食い違い(列名や日付の形式のずれ)はユニットテストでは見えず、画面の導線が途切れる不具合は通しで動かさないと見えません。段ごとに捕まえられるバグの種類が違うので、どの段も必要で、配分が問題なのです。

下の部品で確かめてください。段の右端の持ち手を左右にドラッグすると、その段のテストの本数が変わります。「CI を実行」を押すと、速いユニットテストから順にランプが点き、仕込まれた6つのバグのどれが捕まり、落ちたテストから原因の場所まで分かるかが出ます。

⚡ 体験: テストの段を積んで CI を回す
E2E8本 × 90秒E2E #1E2E #2E2E #3E2E #4E2E #5E2E #6E2E #7E2E #8統合3本 × 20秒統合 #1統合 #2統合 #3ユニット5本 × 2秒ユニット #1ユニット #2ユニット #3ユニット #4ユニット #5CI の実行時間予算 15分13分10秒
L1合計金額の端数の丸め—
L2ちょうど1万円の割引の境界—
L3送料無料の条件の組み合わせ—
S1注文テーブルの列名の食い違い—
S2決済 API の日付形式のずれ—
F1購入ボタンが次の画面へ進まない—
捕まえたバグ—場所まで分かる—無関係な赤(不安定)—CI13分10秒

いまは上の E2E が太い「逆ピラミッド」です。まず「CI を実行」を押してみてください。そのあと、段の右端の持ち手を左右にドラッグして本数を変え、もう一度走らせて比べます。

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

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

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

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

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

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

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

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

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

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

試問

ネットショップのコードに、計算の境界・部品のつなぎ目・画面の導線にまたがる6つのバグが仕込まれています。ユニット(関数を単独で試す。1本2秒)・統合(部品をつないで試す。1本20秒)・E2E(画面を通しで動かす。1本90秒)の3段の右端の持ち手をドラッグして本数を決め、「CI を実行」を押すと、どのバグが捕まり、落ちたテストから原因の場所まで分かるかが出ます。いまは E2E が太い逆ピラミッドです。

✦ 命題挑戦中

CI を15分以内に収めたまま、仕込まれた6つのバグをすべて捕まえ、5件以上は原因の場所まで分かる配分にせよ

  • CI を実行して、6つのバグをすべて捕まえる
  • そのうち5件以上は、落ちたテストから場所まで分かる
  • CI は予算の 15分以内
⚡ 体験: テストの段を積んで CI を回す
E2E8本 × 90秒E2E #1E2E #2E2E #3E2E #4E2E #5E2E #6E2E #7E2E #8統合3本 × 20秒統合 #1統合 #2統合 #3ユニット5本 × 2秒ユニット #1ユニット #2ユニット #3ユニット #4ユニット #5CI の実行時間予算 15分13分10秒
L1合計金額の端数の丸め—
L2ちょうど1万円の割引の境界—
L3送料無料の条件の組み合わせ—
S1注文テーブルの列名の食い違い—
S2決済 API の日付形式のずれ—
F1購入ボタンが次の画面へ進まない—
捕まえたバグ—場所まで分かる—無関係な赤(不安定)—CI13分10秒

いまは上の E2E が太い「逆ピラミッド」です。まず「CI を実行」を押してみてください。そのあと、段の右端の持ち手を左右にドラッグして本数を変え、もう一度走らせて比べます。