【PHPテスト入門 第1回】なぜテストを書くのか:手動確認の限界とテストの種類

PHP
B!

はじめに

この連載は、PHPは書けるけれどテストコードは書いたことがない、という方に向けた入門シリーズです。素のPHPとPHPUnitで基礎を固め、後半ではLaravelでのテストまで扱います。

「テストが大事なのはわかっているけれど、何から始めればいいかわからない」「書いてみたけれど、効果が実感できなかった」。そんな方が、実務で自信を持ってテストを書けるようになることを目指します。

第1回の今回は、コードはほとんど書きません。次の3つを理解して、テストを書く「理由」をはっきりさせることが目的です。

  • 手動の動作確認だけでは、なぜ不十分なのか
  • テストコードがあると、開発の何が変わるのか
  • テストにはどんな種類があり、どう組み合わせるのがよいのか

理由がわかっていると、第2回以降で出てくるテクニックの一つひとつが「なぜこう書くのか」と結びつき、ぐっと理解しやすくなります。

手動の動作確認には限界がある

テストコードを書かない場合、動作確認はブラウザで画面を操作したり、var_dump() で値を表示したりして、人の目で行います。小さなプログラムならそれで十分です。しかし、コードが育っていくにつれて、次の3つの壁にぶつかります。

壁1:確認の手間が、機能の数だけ増えていく

ECサイトのカートを例に考えてみましょう。最初は「商品を入れたら合計金額が出る」だけでした。そこに数量、消費税、セール割引、在庫チェックと機能が増えていきます。

新しい機能を1つ追加したとき、本当に確認すべきなのは新機能だけではありません。既存の機能が壊れていないかも確認する必要があります。機能が10個あれば、変更のたびに10個分の確認が必要です。やがて「たぶん大丈夫だろう」と確認を省くようになり、そこからバグが入り込みます。

壁2:人は同じ確認を正確に繰り返せない

手動の確認は、毎回少しずつ手順や入力値が変わります。前回は数量を3にして試したのに、今回は1で試してしまった。それだけで、数量の計算に入ったバグを見逃します。

実際にこの連載の第2回では、合計金額の計算で「数量を掛け忘れる」バグをわざと入れてみます。数量1で確かめている限り、このバグは表に出ません。

壁3:そもそも手で再現できない条件がある

第4回では、セール割引の終了判定に次のようなバグが入り込む例を扱います。

  • セールは11月30日 23:59:59 まで
  • ところが、条件式の書き間違いで、23:59:59 ちょうどだけ定価になってしまう

このバグが影響するのは、1年のうちたった1秒間です。この瞬間を狙って手で確認するのは、まず不可能でしょう。

端数処理も同様です。第3回では、「税込金額は1円未満を切り捨て」の仕様なのに、四捨五入してしまうバグを扱います。105円の商品を買ったときだけ1円ずれる、といった種類のバグは、よほど意識して入力値を選ばない限り、画面を触っていても気づけません。

手動の確認がダメだということではありません。見た目や使い心地の確認は、人の目でしかできません。ただ、「計算が正しいか」「条件分岐が正しいか」のように、正解がはっきり決まっている確認は、人よりプログラムのほうがはるかに得意です。

テストコードがあると何が変わるか

テストコードとは、「この入力なら、この結果になるはず」という確認を、プログラムとして書いておくものです。たとえば、第2回で最初に書くテストはこれだけです。

public function test_数量を指定すると単価×数量が合計金額になる(): void
{
    $cart = new Cart();
    $cart->add('りんご', 150, 3);
    $cart->add('みかん', 80, 2);

    $this->assertSame(610, $cart->total());
}

150円のりんごを3個、80円のみかんを2個入れたら、合計は610円になるはず。そう書いてあるだけです。細かい書き方は第2回で説明するので、今は雰囲気がつかめれば十分です。

こうしたテストを書いておくと、開発に次の4つの変化が起きます。

1. 全機能の確認が、コマンド1つ・数秒で終わる

書いたテストは、コマンド1つで何度でも、まったく同じ手順で実行できます。第4回を終えた時点で、この連載のテストは15件になります。それらをすべて実行しても、1秒もかかりません。

...............                                                   15 / 15 (100%)

OK (15 tests, 18 assertions)

変更のたびに全機能を確認できるので、「前に直したはずのバグが、別の修正で復活していた」という事態(デグレード)を防げます。

2. コードを直すのが怖くなくなる

動いているコードを読みやすく書き直すこと(リファクタリング)は、本来とても大切な作業です。しかし、テストがないと「書き直して壊れたらどうしよう」と手が止まり、読みにくいコードがそのまま残り続けます。

テストがあれば、書き直した後に実行して、すべて通れば「振る舞いは変わっていない」と確認できます。第3回では、税込計算の実装を round() から整数計算に書き換えますが、テストが通ることで安心して変更できることを体験します。

3. テストが「動く仕様書」になる

テストの名前を工夫しておくと、実行結果がそのまま仕様の一覧になります。第3回で作るテストを実行すると、次のように表示されます。

Cart (Tests\Cart\Cart)
 ✔ 空のカートの合計金額は0円
 ✔ 数量を指定すると単価×数量が合計金額になる
 ✔ 単価がマイナスなら例外を投げる
 ✔ 数量が0なら例外を投げる
 ✔ 税抜0円の税込金額は0円
 ✔ 税抜100円の税込金額は110円
 ✔ 税抜105円の税込金額は115円
 ✔ 税抜999円の税込金額は1098円

WordやExcelの仕様書は、コードを変えても更新し忘れれば古いままです。テストは、コードと食い違えば失敗して知らせてくれるので、常に最新の状態が保たれます。

4. コードの設計が良くなる

テストを書こうとすると、「このクラスは、現在時刻に依存していてテストしにくい」といった設計上の問題に早く気づけます。第4回では、テストしにくいコードをテストしやすい形に作り変えます。その結果、コードは部品の差し替えがしやすく、変更に強い形になります。

テストしやすいコードは、多くの場合、良い設計のコードでもあるのです。

テストの種類とテストピラミッド

ひと口にテストといっても、確かめる範囲によっていくつかの種類があります。代表的なのは次の3つです。

種類確かめる範囲例実行速度
ユニットテスト1つのクラスやメソッドCart の合計金額の計算が正しいかとても速い(1件あたり数ミリ秒)
統合テスト複数のクラスや、DBなどの外部との組み合わせ注文データがDBに正しく保存されるかやや遅い
E2Eテストユーザーの操作から画面表示まで、システム全体ブラウザで商品をカートに入れ、購入完了画面まで進めるか遅い

E2Eは「End to End(端から端まで)」の略です。範囲が広いテストほど、実際の使われ方に近い確認ができます。その一方で、実行に時間がかかり、画面のちょっとした変更でも壊れやすくなります。

テストピラミッド

それぞれのテストを、どのくらいの割合で書けばよいのでしょうか。その目安としてよく知られているのが テストピラミッド という考え方です。

速くて安定しているユニットテストを土台として一番多く書き、統合テストは要所に、E2Eテストは重要な操作の流れに絞って少なく書く、というバランスを表しています。

逆の形(E2Eテストばかりで、ユニットテストが少ない)になると、テストの実行に何十分もかかり、ちょっとした画面の変更でテストが大量に壊れます。やがて誰もテストを実行しなくなり、せっかく書いたテストが役に立たなくなってしまいます。

この連載で扱う範囲

この連載では、ピラミッドの土台であるユニットテストを中心に据えます。

  • 第2〜5回:ユニットテストの基礎(書き方、テストしやすい設計、テストダブル)
  • 第6回:DBを使った統合テスト
  • 第7・8回:LaravelでのHTTPテストなど、フレームワークの機能を使ったテスト

E2Eテストは専用のツール(ブラウザを自動操作するものなど)を使う別の分野になるため、この連載では扱いません。まずは土台をしっかり固めることが、テストを長く活かす一番の近道です。

【画像:上のテストピラミッドの図を画像にして、ブログに差し込む】

テストについてのよくある誤解

テストを書き始める前に、よく耳にする誤解を3つ解いておきましょう。

誤解1:テストを書くと、開発が遅くなる

テストを書く時間がかかるのは事実です。ただし、それは「後で払うはずだった時間」を前払いしているだけです。

テストがない場合、変更のたびに手動で確認する時間、本番で見つかったバグを調べる時間、「どこを直すと何が壊れるかわからない」と悩む時間がかかり続けます。こうした時間は、コードが大きくなるほど増えていきます。テストを書く時間は最初こそかかりますが、書いたテストは何度でもタダで再実行できます。

誤解2:テストがあれば、バグはなくなる

テストが確かめられるのは、テストに書いたことだけです。確かめていないケースにバグがあれば、テストがすべて通っていても、バグは残ります。

第3回では、「0円」と「端数なし」のケースだけでは見逃してしまう端数処理のバグが登場します。テストを書くときは、「どの入力を試せばバグが見つかるか」を考えることが大切です。この連載では、そのための考え方(境界値など)も紹介していきます。

誤解3:コードをすべてテストで網羅しなければならない

テストが本番コードのどれだけの行を実行したかを表す「カバレッジ」という指標があります。この数値を100%にすることを目標にしてしまうのも、よくある落とし穴です。

数値を上げること自体が目的になると、「実行はしているが、結果を何も確かめていないテスト」が量産されがちです。大切なのは、壊れたら困る部分を、意味のある形で確かめることです。カバレッジとの付き合い方は、第9回で詳しく扱います。

連載の全体像

この連載は全10回で、3部構成です。連載を通して、ECサイトの「カート」を題材に、少しずつ機能とテストを育てていきます。

回タイトル主な内容
第1回なぜテストを書くのか手動確認の限界、テストの種類、テストピラミッド(今回)
第2回PHPUnitの環境構築と最初のテストComposerでの導入、phpunit.xml、テストの実行と失敗の読み方
第3回読みやすいテストの書き方AAAパターン、アサーション、例外のテスト、データプロバイダ
第4回テストしやすいコードの書き方依存性注入(DI)、時刻の固定、境界のテスト
第5回テストダブルスタブ・モック・フェイクの違いと使い分け
第6回DBを使うテストテストデータの準備と後片付け
第7回Laravelのテスト基盤Unit と Feature、HTTPテスト、Factory
第8回Laravelのフェイク機能とPestメールやキューのフェイク、Pestの紹介
第9回カバレッジとCIカバレッジの計測と付き合い方、GitHub Actionsでの自動実行
第10回TDDとレガシーコードテスト駆動開発の実演、既存コードへのテスト導入

第1部(第2〜6回)は素のPHPとPHPUnitで基礎を固めます。第2部(第7・8回)では、そこで身につけた考え方をLaravelに持ち込みます。第3部(第9・10回)は、実務でテストを続けていくための実践編です。

読み進めるのに必要なもの

  • PHPの基本的な文法(クラス、名前空間、型宣言)がわかること
  • ターミナルでコマンドを実行できること
  • PHP 8.4以上とComposerが使える環境(第2回で準備します)

記事中のコードと実行結果は、すべて実際に動かして確認しています。

まとめ

  • 手動の動作確認は、機能が増えるほど手間が増え、同じ確認を正確に繰り返せず、再現できない条件もある
  • テストコードがあると、全機能の確認が数秒で終わり、安心してコードを直せ、テストが動く仕様書になり、設計も良くなる
  • テストにはユニット・統合・E2Eの3種類があり、速いユニットテストを土台にしたピラミッド型のバランスがよい
  • テストはバグをゼロにする魔法ではなく、カバレッジ100%も目的ではない。壊れたら困る部分を、意味のある形で確かめる

次回予告

第2回は「PHPUnitの環境構築と最初のテスト」です。PHPとComposerの準備からPHPUnitの導入、最初のテストを書いて実行するところまでを、手順どおりに進めていきます。わざとバグを入れて、テストが失敗する様子も体験します。

参考資料

B!
← 一覧へ戻る