🐡

【Laravel】PHPUnitを使ったテストの手順

2024/04/20に公開

はじめに

どうも、APPRENTICE4期生のKazukiです!

APPRENTICEの課題でテストコードを書くというものがありました。
用語やどういう手順でテストを行うのかが全くわからず苦戦したので同じような人のために共有します。

APPRENTICEの概要はこちら!↓
https://apprentice.jp/

Laravelの公式ドキュメント↓
https://laravel.com/docs/10.x/testing#main-content

使うファイルの説明

まずLaravelのテスト関係のファイルとフォルダの説明をします。

・phpunit.xml
PHPUnitの設定を管理するためのファイル
(PHPUnitは変更した箇所で要件を満たしていて、既存の機能に影響がないかを毎回確認することを自動化できるPHPの単体テストを行うためのFW)

.env.testing
.envの代わりに使用されるファイル
PHPUnitテストを実行するとき、または--env=testingオプションを指定してArtisanコマンドを実行するときに使われる。

・PostControllerTest.php
今回テストコードを書くファイル
HTTPリクエストと投稿ができたかどうかをテストします。

・tests/Feature
統合テストのファイルが入るフォルダ

・tests/Unit
単体テストのファイルが入るフォルダ

※php artisan make:test コマンドを実行するとデフォルトでFeatureフォルダにテストファイルが作られます。

単体テストと統合テストとは?

補足ですが、それぞれの違いをまとめておきます!
単体テスト
・関数やメソッドを個別にテスト
・外部の依存関係を持たないように、モックやスタブを使い隔離されたテストを行う
・コードの正しさを早期に検出し、バグを局所化する

統合テスト
・複数のコンポーネントやモジュールを組み合わせて、それらが一緒に動作するか
・DBやファイルシステムなどの外部リソースとの連携をテスト
・システムの異なる部分が正しく統合され、期待通り動くか
・テスト実行速度は単体テストに比べ遅くなる傾向がある

つまり、単体テストでコードの正しさを確保&統合テストでコンポーネント間の連携を検証することで、システム全体の信頼性を高めるってことですね。

モックとスタブってなにって方はこちらの記事が参考になります↓
https://zenn.dev/shun57/articles/1fd956346c4381

それぞれのファイルの具体例

・.env.testing
データベースに接続するための情報を入れておきます

APP_ENV=testing
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=testing
DB_USERNAME=your_user_name
DB_PASSWORD=your_password

・phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="mysql"/>
        <server name="DB_DATABASE" value="testing"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

以下用語の解説です。

<testsuites>: テストスイートの設定を定義
テストスイートは、関連するテストケースをグループ化し、まとめて実行するために使用されます。

<testsuite name="Unit">: Unitテストのテストスイートを定義
./tests/Unitディレクトリ内のTest.phpで終わるファイルがUnitテストとして実行されます。

<testsuite name="Feature">: Featureテストのテストスイートを定義
./tests/Featureディレクトリ内のTest.phpで終わるファイルがFeatureテストとして実行されます。

<coverage>: コードカバレッジの設定を定義
・テストがコードをどの程度網羅しているかを示す指標

<include>: コードカバレッジの対象となるディレクトリを指定
ここでは./appディレクトリ内の.phpファイルが対象となります。

<server name>: データベース接続設定
<server name="APP_ENV" value="testing"/>: アプリケーションの環境をtestingに設定します。
<server name="DB_CONNECTION" value="mysql"/>: テスト環境でMySQLデータベースを使用することを指定します。
<server name="DB_DATABASE" value="laravel"/>: テスト用のデータベース名を指定します。.env.testingファイルで指定した値と一致させます。

他の設定項目は、テストの要件に応じて適宜設定します。例えば、メールのテストを行う場合は<server name="MAIL_MAILER" value="array"/>を指定して、メールが実際に送信されないようにしたりできます。

HTTPリクエストテスト

以下のコマンドでPostControllerTest.phpを作ります

php artisan make:test PostControllerTest

・PostControllerTest.php

public function test_index_returns_200()
{
    $response = $this->get(route('conduit.index'));
    $response->assertStatus(200);
}

まずステータスコード200は、webサーバーにリクエストを送信した時に正常に接続できた時に返ってくる値です。(他には404 Not Foundや502 Bad Gatewayなどがあります)

$response = $this->get(route('conduit.index'));
$this->get()メソッドを使用して、conduit.indexルートに対してGETリクエストを送信します。
route('conduit.index')は、名前付きルートを使用してURLを生成します。conduit.indexは、routes/web.phpファイルで定義されているルート名です。
GETリクエストの結果は、$response変数に格納されます。

$response->assertStatus(200);
$response変数に格納されたレスポンスのHTTPステータスコードが200であることをアサートします。
assertStatus(200)は、レスポンスのステータスコードが200(OK)であることを期待しています。
もしレスポンスのステータスコードが200でない場合、このアサーションは失敗し、テストは失敗します。

投稿のテスト

・PostControllerTest.php

public function testStore()
    {
        // テストデータの準備
        $user = User::factory()->create();
        $postData = [
            'title' => 'Test Title',
            'about' => 'Test About',
            'content' => 'Test Content',
            'tags' => 'tag1,tag2,tag3',
        ];

        // リクエストの送信
        $response = $this->actingAs($user)->post(route('conduit.store'), $postData);

        // レスポンスの検証
        $response->assertRedirect(route('conduit.index'));
        $response->assertSessionHas('success', '投稿が完了しました');

        // データベースの検証
        $this->assertDatabaseHas('posts', [
            'title' => 'Test Title',
            'about' => 'Test About',
            'content' => 'Test Content',
        ]);

        // タグの検証
        $post = Post::where('title', 'Test Title')->first();
        $this->assertEquals(['tag1', 'tag2', 'tag3'], $post->tags->pluck('name')->toArray());
    }

以下、コードの解説です。

$user = User::factory()->create();

Userファクトリを使用して、テスト用のユーザーを作成しています。

Copy code
    $postData = [
        'title' => 'Test Title',
        'about' => 'Test About',
        'content' => 'Test Content',
        'tags' => 'tag1,tag2,tag3',
    ];

テストデータとして、$postData配列を定義しています。
title、about、content、tagsのそれぞれにダミーのデータを設定しています。

$response = $this->actingAs($user)->post(route('conduit.store'), $postData);

actingAsメソッドを使用して、作成したテストユーザーを認証状態にしています。
postメソッドを使用して、conduit.storeルートにPOSTリクエストを送信しています。
リクエストデータとして、postData配列を渡し、レスポンスをresponse変数に代入。

$response->assertRedirect(route('conduit.index'));

assertRedirectメソッドを使用して、レスポンスがリダイレクトされることを検証。
リダイレクト先がconduit.indexルートであることを確認しています。

$response->assertSessionHas('success', '投稿が完了しました');

assertSessionHasメソッドを使用して、セッションにサクセスメッセージが存在することを検証。successキーに'投稿が完了しました'という値が設定されていることを確認しています。

    $this->assertDatabaseHas('posts', [
        'title' => 'Test Title',
        'about' => 'Test About',
        'content' => 'Test Content',
    ]);

assertDatabaseHasメソッドを使用して、データベースに新しい投稿が保存されていることを検証。
postsテーブルに、指定した条件(title、about、contentの値)に一致するレコードが存在することを確認しています。

$post = Post::where('title', 'Test Title')->first();

whereメソッドとfirstメソッドを使用して、titleが'Test Title'である投稿を取得しています。

$this->assertEquals(['tag1', 'tag2', 'tag3'], $post->tags->pluck('name')->toArray());

assertEqualsメソッドを使用して、取得した投稿に関連付けられたタグを検証。
$post->tagsでタグのコレクションを取得し、pluck('name')でタグ名の配列を取得。
toArray()でタグ名の配列を通常の配列に変換しています。
['tag1', 'tag2', 'tag3']と一致することを確認。

ファクトリとシーダー

テストでデータを作成するときに使える機能としてファクトリとシーダーがあります。
それぞれの違いを紹介して終わりにしようとおもいます!

ファクトリ

テストコードで以下の記述があったと思います。

$user = User::factory()->create();

これはファクトリといってテストを実行してる時だけ自動的に生成されるテストデータです。
この場合UserFactory.phpを参考にユーザーデータが作られ、testStoreメソッドが終わるまで使われるという感じです。

ファクトリはテストの独立性を維持するために使われるため、DBを見てもデータは入っていません。

シーダー
シーダーは実際にテストのDBにデータを挿入して開発環境の初期設定をするために使われます。

以下はシーダーを使ってユーザーデータを作る流れです。

php artisan make:seeder UserSeeder

UserSeeder.php

use App\Models\User;

public function run()
{
    User::factory()->create([
        'name' => 'Test User',
        'email' => 'test@example.com',
        // 他の必要な属性を指定
    ]);
}

ファイルを定義したら以下のコマンドを実行してデータを登録します

php artisan db:seed

ファクトリとシーダーを適切に使い分けて、テストデータの管理と開発環境の初期設定を効果的に行っていきたいですね。

まとめ

今回はPHPUnitを使った基本的なテストの流れをまとめてみました。

テストなんて簡単かなと考えていた自分にアホと言いたい、、
この世界は本当に広くてたまに絶望しますが、その分だけ学び、できるようになっていくところが楽しいなと感じる今日この頃です。

また、先にテストを作って実装を行い都度テストをパスするか確認して進めるTDD(テスト駆動型開発)もあると知ったので今後はこちらも意識して学習していきたいと思います!

ではまた!

Discussion