単体テストは、ソフトウェア開発の肝であり、アプリケーションのコンポーネントがそれぞれ期待通りに動作することを保証するために欠かせません。特定のコード単体に対してテストを書くことで、開発初期段階でエラーを特定し、修正することができます。

継続的インテグレーションおよび継続的デリバリー(CI/CD)パイプラインでは、コードの変更後、自動でこのテストを実行することができます。新たに書いたコードでエラーが生じたり、既存の機能が壊れたりするのを回避するのに有用です。

今回は、Laravelアプリケーションにおける単体テストの重要性を解説し、KinstaのアプリケーションホスティングサービスでデプロイしたLaravelアプリケーションの単体テストの書き方を詳しくご紹介していきます。

PHPUnit

PHPUnitは、PHPエコシステム内で広く使用されている単体テスト向けのフレームワークです。テストの作成と実行に便利なツール群が揃っており、コードベースの高い信頼性と品質を確保するための重要なソリューションになります。

Laravelは、PHPUnitを使ったテストをサポートしており、 アプリケーションのテストに役立つメソッドも提供しています。

LaravelプロジェクトでPHPUnitをセットアップするのに設定はほとんど不要です。phpunit.xmlファイルやテストファイル専用のtestsディレクトリなど、すでに適切な設定が行われたテスト環境が用意されています。

テスト環境は、phpunit.xmlファイルを変更してカスタマイズすることも可能です。また、.envファイルを使用する代わりに、プロジェクトのルートフォルダに.env.testing環境ファイルを作成することも。

Laravelデフォルトのテスト環境

Laravelには、デフォルトのディレクトリ構造があります。ルートディレクトリには、Feature サブディレクトリとUnitサブディレクトリを含むtestsディレクトリがあり、この構造により、異なるテストを簡単に区別し、クリーンで整理されたテスト環境を維持することができます。

Laravel プロジェクト内のphpunit.xmlファイルは、 テストプロセスのオーケストレーションやテストの一貫性の確保、 PHPUnit の挙動をプロジェクトの要件に合わせてカスタマイズするために重要です。テストの定義やテスト環境の指定、 データベース接続の設定など、テストの実行方法を定義します。

このファイルでは、セッションやキャッシュ、そしてメールの配列ドライバへの設定も行い、テストの実行時にセッション、キャッシュ、メールのデータが持続しないようにします。

Laravelアプリケーションでは、以下のようなテストを実行することができます。

  • 単体テスト─クラス、メソッド、関数など、コードの個々のコンポーネントをテスト。Laravelアプリケーションから分離されたまま、特定のコード単位が期待通りに動作するかどうかを検証する。tests/Unitディレクトリで定義されたテストは、Laravelアプリケーションを起動することがないため、データベースやフレームワークが提供する他のサービスにはアクセスできない。
  • 機能テスト─アプリケーションの広範な機能をテスト。HTTPリクエストとレスポンスを再現し、ルート、コントローラ、様々なコンポーネントの統合を検証する。アプリケーションのあらゆる部分が適切に連携してスムーズに動作することを保証するのに役立つ。
  • ブラウザテスト─ブラウザインタラクションを自動化し、さらに細部を検証するテスト。フォームへの入力やボタンのクリックなどのユーザーインタラクションを再現するために、ブラウザの自動化とテストツール「Laravel Dusk」を使用。実際のブラウザでのアプリケーションの動作やユーザー体験を検証するのに非常に重要。

テスト駆動開発

テスト駆動開発(TDD)は、コードを実装する前にテストを行うことを重視したソフトウェア開発手法。「レッド」「グリーン」「リファクタリング」のサイクルに従います。

テスト駆動開発のレッド/グリーン/リファクタサイクル
テスト駆動開発のレッド/グリーン/リファクタサイクル

各段階はそれぞれ以下のようになっています。

  • レッド─実際のコードを実装する前に、機能を定義するテスト(新規または既存)を改良する。このテストは失敗を想定。
  • グリーン─失敗したテストを修正するコードを書き、失敗(レッド)したテストを通過(グリーン)させる。完璧なコードにはならないが、対応するテストケースの要件は満たすことができる。
  • リファクタリング─動作を変えずに、可読性、保守性、パフォーマンスを向上させるためにコードをリファクタリングする。この段階で、既存のテストケースがリグレッション(修正した問題が再び現れること)する問題を検出するため、これを心配せずにコードを変更することができる。

TDDには以下のような利点があります。

  • バグの早期発見─開発プロセスの初期段階でバグを検出し、開発サイクルの後半で問題を修正する費用と時間を削減するのに役立つ。
  • 設計の改善─より良いソフトウェア設計のため、疎結合化が推奨される。実装前にインターフェースやコンポーネントの相互作用を考慮する時間を確保することができる。
  • リファクタリングに関する懸念を排除─既存のテストがリファクタリング中に発生したリグレッションを素早く特定するため、安心してコードをリファクタリングすることができる。
  • 生きたドキュメント─テストケースは、コードがどのように動作すべきかの例を提示することで、有用なドキュメントになる。テストに失敗するとコードに問題があることを示すため、このドキュメントは必然的に常に最新の状態に保たれる。

Laravel開発では、コントローラ、モデル、サービスなどのコンポーネントを実装する前にテストを書くことで、TDDの原則に従うことができます。

PHPUnitをはじめとするLaravelのテスト環境には、TDDを促進する便利なメソッドやアサーションが組み込まれているため、有意義なテストを作成し、レッド/グリーン/リファクタのサイクルを効果的に実行できます。

基本的な単体テストの書き方

ここからは、モデルの機能を検証するための基本的なテストの書き方をご紹介します。

前提条件

テストを書くにあたり、以下が必要になります。

  • Laravelに関するこちらのブログ記事に記載されている前提条件を満たしている
  • Laravelアプリケーション─今回は例として上のブログ記事で作成したアプリケーションを使用。同記事を参考にブログアプリを作成することもできますが、テストを実装するためのソースコードのみが必要な場合は、以下の手順に従ってください。
  • Xdebugをインストールし、カバレッジモードを有効にする

プロジェクトをセットアップする

  1. ターミナルウィンドウで以下のコマンドを実行し、プロジェクトを複製。
    git clone https://github.com/VirtuaCreative/kinsta-laravel-blog.git
  2. プロジェクトフォルダに移動し、composer installコマンドでプロジェクトの依存関係をインストール。
  3. env.exampleファイルの名前を「.env」に変更。
  4. php artisan key:generateコマンドを実行してアプリキーを生成。

テストの作成と実行

まず、自分のマシンにプロジェクトのコードがあることを確認してください。テストするモデルは、app/Http/Models/Post.phpファイルで定義されているPostです。このモデルには、titledescriptionimageなどのfillable属性が含まれています。

このモデルの単体テストを作成していきます。1つは属性が適切に設定されていることを確認し、もう1つは非fillable属性の割り当てを試みることで、一括代入を検証します。

  1. php artisan make:test PostModelFunctionalityTest --unitコマンドを実行し、新規テストケースを作成する。--unitは、単体テストであることを示し、テストケースをtests/Unit ディレクトリに保存する。
  2. tests/Unit/PostModelFunctionalityTest.phpファイルを開き、test_example関数を以下のコードに置き換える。
    public function test_attributes_are_set_correctly()
       {
           // 属性を持つ新しい投稿インスタンスを作成
           $post = new Post([
               'title' => 'Sample Post Title',
               'description' => 'Sample Post Description',
               'image' => 'sample_image.jpg',
           ]);
    
           // 属性が正しく設定されているかを確認
           $this->assertEquals('Sample Post Title', $post->title);
           $this->assertEquals('Sample Post Description', $post->description);
           $this->assertEquals('sample_image.jpg', $post->image);
       }
    
       public function test_non_fillable_attributes_are_not_set()
       {
           // 追加属性を持つ投稿の作成を試みる(非fillable)
           $post = new Post([
               'title' => 'Sample Post Title',
               'description' => 'Sample Post Description',
               'image' => 'sample_image.jpg',
               'author' => 'John Doe',
           ]);
    
           // postインスタンスに非fillable属性が設定されていないことを確認
           $this->assertArrayNotHasKey('author', $post->getAttributes());
       }

    このコードでは、2つのテストメソッドを定義しています。

    最初のメソッドは、指定した属性を持つPostインスタンスを作成し、アサーションメソッドassertEqualsを使用してtitledescriptionimage属性を正しく設定したことをアサーションします。

    2つ目のメソッドは、追加の非fillable属性(author)を持つPostインスタンスの作成を試み、アサーションメソッドassertArrayNotHasKeyを使用して、この属性がモデルインスタンスに設定されていないことをアサーションします。

  3. 同じファイルに以下のuse文を追加。
    use App\Models\Post;
  4. php artisan config:clearコマンドを実行して、構成キャッシュをクリア。
  5. 最後に、以下のコマンドでテストを実行。
    php artisan test tests/Unit/PostModelFunctionalityTest.php

    これですべてのテストが通過し、ターミナルにテストの結果と総実行時間が表示されるはずです。

テストのデバッグ

テストが失敗した場合は、以下の手順でデバッグすることができます。

  1. ターミナルでエラーメッセージを確認。Laravelは、問題を特定する詳細なエラーメッセージを提供してくれるため、内容にしっかり目を通し、テストが失敗した理由を理解する。
  2. テストしているテストとコードを点検し、矛盾点を特定する。
  3. テストに必要なデータや依存関係を適切にセットアップしているかを確認。
  4. Laravelのdd()のようなデバッグツールを使用して、テストコードの特定の箇所で変数やデータを検証。
  5. 問題を特定したら必要な変更を加え、通過するまでテストを繰り返す。

テストとデータベース

Laravelでは、SQLiteのインメモリデータベースを使用してテスト環境をセットアップすることができます。テスト用データベース環境を設定し、データベースとやり取りするテストを書く手順を見ていきましょう。

  1. phpunit.xmlファイルを開き、以下のコードの行をアンコメント(コメント状態を解除)する。
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
  2. php artisan make:test PostCreationTest --unitコマンドを実行し、新規テストケースを作成。
  3. tests/Unit/PostCreationTest.phpファイルを開き、test_exampleメソッドを以下のコードに置き換える。
    public function testPostCreation()
       {
           // 新規投稿を作成してデータベースに保存
           $post = Post::create([
               'title' => 'Sample Post Title',
               'description' => 'Sample Post Description',
               'image' => 'sample_image.jpg',
           ]);
    
           // データベースから投稿を取得し、その存在をアサートする
           $createdPost = Post::find($post->id);
           $this->assertNotNull($createdPost);
           $this->assertEquals('Sample Post Title', $createdPost->title);
       }
  4. 以下のuse文を追加する。
    use App\Models\Post;

    PostCreationTestクラスは、PHPUnitFrameworkTestCaseベースクラスを継承しています。このベースクラスは、Laravelの外でPHPUnitを使用する場合や、Laravelと緊密に結合していないコンポーネントのテストを書く場合の単体テストによく使われます。しかし、データベースにアクセスする必要があるため、PostCreationTestクラスを変更し、TestsTestCaseクラスを継承しなければなりません。

    後者のクラスは、PHPUnitFrameworkTestCaseクラスをLaravelアプリケーション用に調整し、データベースのシードやテスト環境の設定など、追加機能やLaravel特有のセットアップを提供します。

  5. use PHPUnitFrameworkTestCase;use TestsTestCase;に置き換える。SQLiteインメモリデータベースを使用するようにテスト環境を設定しているため、テスト実行前にデータベースの移行が必要になります。これには、IlluminateFoundationTestingRefreshDatabaseを使用します。このトレイトは、スキーマが最新でない場合にデータベースをマイグレーションし、各テスト後にデータベースをリセットして、前のテストのデータが後続のテストに干渉しないようにします。
  6. tests/Unit/PostCreationTest.phpファイルにuseを追加して、このトレイトを組み込む。
    use Illuminate\Foundation\Testing\RefreshDatabase;
  7. testPostCreationメソッドの直前に次の行を追加。
    use RefreshDatabase;
  8. php artisan config:clearコマンドを実行してコンフィギュレーションキャッシュをクリアする。
  9. 以下のコマンドでテストを実行。
    php artisan test tests/Unit/PostCreationTest.php

    これでテストが通過し、ターミナルにテスト結果と合計テスト時間が表示されます。

機能テスト

単体テストが個々のアプリケーションコンポーネントを個別に検証するのに対して、機能テストは、複数のオブジェクトがどのように相互作用するかなど、単体テストよりも包括的に検証を行うものです。機能テストもまた、以下のような理由から非常に重要です。

  1. エンドツーエンドの検証─コントローラ、モデル、ビュー、データベースのような様々なコンポーネント間の相互作用を含め、機能全体がシームレスに動作することを確認できる。
  2. エンドツーエンドのテスト─最初のリクエストから最終的なレスポンスまでのユーザーフロー全体を網羅し、単体テストでミオと割れる可能性のある問題を検出可能。ユーザージャーニーや複雑なシナリオのテストに有用。
  3. 優れたユーザーエクスペリエンスの確保─ユーザーのインタラクションを再現し、一貫したユーザー体験と機能が意図した通りに機能することを検証。
  4. リグレッションの検出─新たにコードを導入する際に、リグレッションやコードを壊すような変更を検出。既存の機能が機能テストで失敗すると、何かが壊れたことを知らせてくれる。

例として、app/Http/Controllers/PostController.phpファイルにPostControllerの機能テストを作成していきます。storeメソッドに焦点を当て、入力されたデータを検証し、投稿を作成してデータベースに保存します。

このテストでは、ユーザーがウェブインターフェースから新規投稿を作成することを再現し、コードが投稿をデータベースに保存し、作成後に投稿一覧ページにリダイレクトすることを確認します。手順は以下のようになります。

  1. php artisan make:test PostControllerTestコマンドを実行し、tests/Featuresディレクトリに新規テストケースを作成。
  2. tests/Feature/PostControllerTest.phpファイルを開き、test_exampleメソッドを次のコードに置き換える。
    use RefreshDatabase; // 各テスト後にデータベースをリフレッシュ
    
       public function test_create_post()
       {
           // ウェブインターフェースから新規投稿を作成するユーザーを再現
           $response = $this->post(route('posts.store'), [
               'title' => 'New Post Title',
               'description' => 'New Post Description',
               'image' => $this->create_test_image(),
           ]);
    
           // 投稿がデータベースに正常に格納されたことをアサート
           $this->assertCount(1, Post::all());
    
           // 投稿作成後、ユーザーが投稿インデックスページにリダイレクトされることを保証
           $response->assertRedirect(route('posts.index'));
       }
    
       // 投稿用のテスト画像を作成するヘルパー関数
       private function create_test_image()
       {
           // LaravelのUploadedFileクラスを使ってモック画像ファイルを作成
           $file = UploadedFile::fake()->image('test_image.jpg');
    
           // 一時的な画像ファイルへのパスを返す
           return $file;
       }

    test_create_post関数は、LaravelのUploadedFileクラスを使用して生成されたモック画像を含む特定の属性を持つposts.storeルートにPOSTリクエストを行うことで、ユーザーが新規投稿を作成するのを再現します。

    このテストでは、Post::all()のカウントをチェックすることで、コードが正常に投稿をデータベースに保存したことを確認します。 投稿作成後、コードがユーザーを投稿一覧ページにリダイレクトすることを確認します。

    これにより、投稿作成機能が適切に動作し、アプリケーションが投稿後のデータベースとのやりとりとリダイレクトを正しく処理することを保証することができます。

  3. 同じファイルに以下のuse文を追加する。
    use App\Models\Post;
    use Illuminate\Http\UploadedFile;
  4. php artisan config:clearコマンドを実行して、コンフィギュレーションキャッシュをクリアする。
  5. 以下のコマンドでテストを実行。
    php artisan test tests/Feature/PostControllerTest.php

    これでテストに通過し、ターミナルにテスト結果とテスト実行の合計時間が表示されます。

テストカバレッジの確認

テストカバレッジは、単体テストや機能テスト、またはブラウザテストが、 コードベースのどれだけの部分を確認するかをパーセンテージで表したものです。コードベースのテストされていない部分や、バグを含む可能性があるテストが不十分な部分を特定するのに役立ちます。

PHPUnitのコードカバレッジ機能やLaravel組み込みのカバレッジレポートなどのツールは、コードベースのどの部分がテスト対象になっているかを示すレポートを生成してくれます。テストの質に関する貴重なインサイトを提供し、追加のテストが必要と思われる部分に焦点を当てるのに役立ちます。

レポートの作成

  1. tests/Feature/ExampleTest.phpファイルとtests/Unit/ExampleTest.phpファイルを削除。
  2. ターミナルウィンドウでphp artisan test --coverageコマンドを実行。出力は以下のようになります。

    php artisan test -coverageコマンドを実行
    php artisan test -coverageコマンドを実行

    コードカバレッジレポートには、テスト結果、通過したテストの総数、結果の実行時間が表示されます。また、コードベースの各コンポーネントとそのコードカバレッジのパーセンテージも表示されます。パーセンテージは、テストがカバーするコードの割合を示します。例えば、Models/Postのカバレッジは100%であることがわかります。 これは、モデルのすべてのメソッドとコード行がテストされていることを意味します。このレポートには、コードベース全体のカバレッジ(Total Coverage)も表示されます。上の例では、全体の65.3%しかテストされていないことがわかります。

  3. php artisan test --coverage --min=85コマンドを実行し、最小カバレッジしきい値を指定。今回は最小しきい値を85%に設定します。以下のような出力になります。

    最低しきい値85%でテストを実行
    最低しきい値85%でテストを実行

    コードが設定された最低しきい値85%を満たしていないため、テストは失敗。

    より高いコードカバレッジ、基本的には100%を達成することが目標になりますが、アプリケーションの重要で複雑な部分を徹底的にテストすることのほうが重要です。

まとめ

包括的で有意義なテストを書くこと、テスト駆動開発(TDD)のレッド/グリーン/リファクタサイクルを守ること、そしてLaravelとPHPUnitのテスト機能を活用するなど、今回ご紹介したベストプラクティスを取り入れて、堅牢で質の高いアプリケーションを開発してみてください。

Laravelアプリケーションを高速かつ安全で信頼性の高いインフラストラクチャでホストするなら、Kinstaをお試しください。Kinsta APIを利用して、GitHub Actions、CircleCIなどのプラットフォームを通じて、CI/CDパイプライン内でデプロイを行うことができます。

Jeremy Holcombe Kinsta

Kinstaのコンテンツ&マーケティングエディター、WordPress開発者、コンテンツライター。WordPress以外の趣味は、ビーチでのんびりすること、ゴルフ、映画。高身長が特徴。