Udemyのオススメ講座はこちら 詳細を見てみる

【Laravel】PHPUnitでテストコードを書くときのTipsやサンプルコード

  • URLをコピーしました!

はじめに、

この記事はLaravel Advent Calendar 2021 17日目の記事としてQiitaに投稿した記事を移設したものです。

僕自身が書いた経験のある範囲でPHPUnitでテストコードを書くときのTipsやサンプルコードをまとめます。

目次

ちょっとした小ネタ

Tips、サンプルコードの前に本記事のタイトルとは直接的には関係ないけど、PHPUnitでテストコードを書く際の小ネタを挟みます。

テスト関連のコマンド

テストファイル作成

# tests/Featureに作成される
php artisan meke:test SampleTest

# tests/Unitに作成される
php artisan make:test SampleTest --unit

# ディレクトリを指定してファイル作成
# tests/Feature/User配下
php artisan make:test User/SampleTest
# tests/Unit/User配下
php artisan make:test User/SampleTest --unit

テスト実行

# 事前実行
php artisan config:clear

# 全テストファイル実行(以下どちらでも可能ですが出力結果のUIが異なります。僕は前者を使っています)
php artisan test
./vendor/bin/phpunit

# 特定のテストファイル実行
php artisan test tests/Feature/PreUserControllerTest.php

テスト用DBの準備

ローカル環境で実行するテストで使うDBの準備はphpunit.xmlのデフォルトでコメンアウトされている2行のコメントを外すだけで、インメモリのSQLiteを使うことができます。

<!-- <server name="DB_CONNECTION" value="sqlite"/> -->
<!-- <server name="DB_DATABASE" value=":memory:"/> -->

これはローカル環境をDockerで構築している場合でも使えるので、わざわざテスト用のDBコンテナを用意する必要なく、DBが絡むテストを実行することができるので個人的にオススメです。

Tipsやサンプルコード

ここからは僕自身がこれまで実装したことのある範囲で

こういうケースはこんな感じでコードが書けば実装できるよ!参考にしてね!

というのをいくつかまとめます。

本記事の内容のベタープラクティス、ベストプラクティスはあると思いますが、参考にしていただけたら幸いです。

ハッシュ化されたパスワードの比較

  • 片方が平文(POSTデータのパラメータとか)
  • もう片方がハッシュ化されている(DBに登録されているデータとか)

の場合の比較にはHash::check()を使う。

/**
* @test 
*/
public function fecth_リクエストパラメータからユーザー情報の取得に成功する(): void
{
    $email = 'test@example.com';
    $password = 'password';

    $service = new FetchService();
    // ユーザー情報取得
    $response = $service->fetch($email, $password);

    // メールアドレスの比較検証
    $this->assertSame($email, $response->email);
    // パスワードの比較検証
    $this->assertTrue(Hash::check($password, $response->password));
}

ちなみに同値チェックにはassertSameassertEqualsがありますが、前者(assertSame)は型も検証要素になる(型も同じかどうかをチェックしてくれる)ので基本的にassertSameを使った方が良いと思います。

Mailaleクラス(メール送信)のテスト

個人的にこの記事を参考にするのが良いかと思います。

一応、下にコードを記載します。

/**
* @test
*/
public function sendMail_メールが1通送信されている()
{
    $email = 'test@example.com';

    // 実際にはメールを送らないように設定
    Mail::fake();
    // メールが送られていないことを確認
    Mail::assertNothingSent();

    $service = new SendMailService();
    $service->sendMail($email);

    // メッセージが指定したユーザーに届いたことをアサート
    // (SampleMailがMailableクラスです)
    Mail::assertSent(SampleMail::class, function ($mail) use ($email) {
        return $mail->hasTo($email);
    });

    // メールが1回送信されたことをアサート
    Mail::assertSent(SampleMail::class, 1);
    }

例外(Exception)のテスト

例外のテストは基本的に以下の2つのメソッドを使う。

// 例外クラスの検証
$this->expectException();
// メッセージの検証
$this->expectExceptionMessage();

基本形は以下の形で書く。

/**
* @test
*/
public function test_例外の検証()
{
    $this->expectException(Exception::class);
    $this->expectExceptionMessage('エラーです。');

    $service = new SampleService();
    $service->testMethod();
}

例外のテストの場合は通常のテストみたいに処理の最後に$this->assertStatus()とかを書かず、$this->expectException()$this->expectExceptionMessage()を1番上に書く。

例外のテストを書く時の注意点は以下のようなにtry-catchの中で例外を投げているメソッドの場合は上記のテストコードでは上手くいかない。

テストを実行するとFailed asserting that exception of type "Exception" is thrown.というエラーになります。

try {
    // 略
    throw new Exception('エラーです');
} catch (Exception $e) {
    throw $e;
}

このような場合の例外のテストでは以下のように1番上に$this->withoutExceptionHandling();を追加すると正常にテストできます。

/**
* @test
*/
public function test_例外の検証()
{
    // これが必要
    $this->withoutExceptionHandling();

    $this->expectException(Exception::class);
    $this->expectExceptionMessage('エラーです。');

    $service = new SampleService();
    $service->testMethod();
}

ソースはこちらです。

ちなみに「PHPUnit 例外 テスト」とかでググると以下のアノテーションで例外の検証する方法がヒットしますが、PHPUnit9系から廃止されています。

/**
* @test
* @expectedException
* @expectedExceptionMessage
*/

PHPUnitのドキュメントの中で「例外のテスト」には上記アノテーションを使った方法は紹介されていません。(アノテーション自体は掲載されている…)

UnitテストでFactoryのunique()、safeEmail()を使うとき

Unitテストファイルをコマンド(php artisan make:test SampleTest --unit)で生成するときは自動的にテストクラスの継承元のスーパークラスがPHPUnit\Framework\TestCaseとなります。

このままの状態でモデルFactoryでテスト用データを生成する時にunique()safeEmail()を使うと以下のエラーになります。

Unknown formatter "unique"
Unknown formatter "safeEmail"

対策としてはUnitテストでもFactoryのunique()safeEmail()を使うときは継承元のスーパークラスをFeatureテストと同じくTests\TestCaseに変更すると実行できるようになります。

(use文をuse PHPUnit\Framework\TestCase;use Tests\TestCase;に変更する)

シーダーファイル実行

以下のコードで既存のシーダーファイル(/database/seeders配下)を実行してテスト用DBにテストデータを登録することができる。

$this->artisan('db:seed', ['--class' => 'SampleTableSeeder']);

$this->artisan()で他のartisanコマンドも実行できます。

actingAsを使った時に出る警告への対応

ログイン状態を実現するためのactingAs()ですが、以下のような書き方ではVSCode上で警告が出ます。

  • テストクラスのプロパティとして$userを定義
  • setUp()の中で$this->userを使ってactingAs()
private $user;

public function setUp(): void
{
    parent::setUp();
    $this->user = User::factory()->create();
    $this->actingAs($this->user, 'web');
}

警告文は以下です。

Expected type ‘Illuminate\Contracts\Auth\Authenticatable’. Found ‘Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model’)

LaraStanというCompoerパッケージで静的解析しても出ます(解析レベル5で試行)

上記コードで警告を解消する方法は2通りあります。

テストクラスのプロパティで指定($user)setUp()内でfactoryでユーザー生成→テストメソッドでactingAs()

private $user;

public function setUp(): void
{
    parent::setUp();
    $this->user = User::factory()->create();
}

/**
* @test
*/
public function test_サンプルのテストメソッドです()
{
    $this->actingAs($this->user, 'web');
    // 略
}

テストメソッド内でfactoryでユーザー生成(@var指定必要)→そのユーザーでactingAs()

ユーザーを生成した時に/** @var \Illuminate\Contracts\Auth\Authenticatable $user */を指定して警告にもあるように$userを'Illuminate\Contracts\Auth\Authenticatable'のインスタンスとして定義してあげる。

/**
* @test
*/
public function test_サンプルのテストメソッドです()
{
    /** @var \Illuminate\Contracts\Auth\Authenticatable $user */
    $user = User::factory()->create();
    $this->actingAs($user, 'web');
    // 略
}

上記の繰り返しになるが、結論としては以下の2つの方法のどちらかで書くのが良いと思われる。

  • テストクラスのプロパティで指定($user)setUp()内でfactoryでユーザー生成→テストメソッドでactingAs()
  • テストメソッド内でfactoryでユーザー生成(@var指定必要)→そのユーザーでactingAs()

DBから取得したデータのテスト

「どこまで厳密に検証するか?」によって書き方が異なるので厳しい順に書きます。

データの中身までチェックする

/**
 * @test
 */
public function fetch_ユーザーデータを全件取得する(): void
{
    // 略($responseにコレクションが格納されているとする)

    // 1つ目のインスタンスを配列に変換する
    $responseArray = $response[0]->toArray();
    $this->assertSame([
        'id',
        'name',
        'email',
        'created_at',
        'updated_at'
    ], array_keys($responseArray));
}

->first()で1件のデータ取得の場合は、レスポンスがモデルインスタンスなので、以下の書き方で配列に変換します。

$responseArray = $response->toArray();

特定のクラスのインスタンスかどうかだけチェックする

instanceofを使って特定のモデルインスタンスであるかどうかを検証する。

// 1件のデータを取得する場合
$this->assertTrue($response instanceof SampleModel)

// コレクションの場合
$this->assertTrue($response[0] instanceof SampleModel)

検証内容は同じですが以下のようにassertInstanceOfというアサーションを使った方がわかりやすいかもです。(いや、こっちの方が断然いいですね)

// 1件のデータを取得する場合
$this->assertInstanceOf(SampleModel::class, $response)

// コレクションの場合
$this->assertInstanceOf(SampleModel::class, $response[0])

オブジェクトかどうかだけチェックする

1番緩い。これならわざわざ検証する必要もないんじゃないか?と思うレベルです。

// 1件のデータを取得する場合
$this->assertIsObject($response);

// コレクションの場合
$this->assertIsObject($response[0]);

JSONでCollectionを返却するメソッドのテスト

先ほどの「DBから取得したデータのテスト」と被るところもありますが、CollectionをJSONに要素として返却するメソッドのテスト考えます。

「DBから取得したデータのテスト」はモデルやサービスのテストのイメージで、こっちはControllerのイメージです。

レスポンスの中身(key・valueの組み合わせまで)を検証する

月火水木金土日のデータを全件取得してJSONを返却するメソッドのテストとします。

/**
 * @test
 */
public function fetch_曜日データを全件取得する(): void
{
    $response = $this->getJson('/api/week');

    $response->assertStatus(200)
        ->assertJson([
            'data' => [
                [
                    'id' => 1,
                    'name' => 月曜,
                ],
                [
                    'id' => 2,
                    'name' => 火曜,
                ],
                [
                    'id' => 3,
                    'name' => 水曜,
                ],
                [
                    'id' => 4,
                    'name' => 木曜,
                ],
                [
                    'id' => 5,
                    'name' => 金曜,
                ],
                [
                    'id' => 6,
                    'name' => 土曜,
                ],
                [
                    'id' => 7,
                    'name' => 日曜,
                ],
            ]
        ]);
}

上記サンプルメソッドのように返却する要素の個数が確定しているときはassertJsonでJSONの中身まで検証するのが良いと思います。(一応assertStatus(200)でレスポンスのステータスも検証しています)

レスポンスの構造を検証する

「投稿データの全件取得」等、データの個数が多い or 確定できない場合は、assertJsonを使って中身を検証するとコードがめちゃくちゃ多くなるので、こういう時はassertJsonStructureを使ってJSONの構造を検証する。

/**
 * @test
 */
public function fetch_投稿データを全件取得する(): void
{
    $response = $this->getJson('/api/posts');

    $response->assertStatus(200)
        ->assertJsonStructure([
            'data' => [
                '*' => [
                    'id',
                    'user_id',
                    'body',
                ]
            ]
        ]);
}

assertJsonStructureを使うことでJSONの構造を検証することができる。

'*'は任意の文字を許容するので配列の添字が入ることで多量のデータの構造を検証することができる。

JsonResponseを配列に変換してテストする

先ほどはJSON型のレスポンス(LaravelではJsonResponse型)をJSONとしてPHPUnitのassert〜メソッドで検証しましたが、JsonResponseを配列に変換してテストする方法もあります。

/**
 * @test
 */
public function fetch_ユーザーデータを全件取得する(): void
{
    // 略($responseにJSONが格納されているとする)

    $responseArray = json_decode($response->content(), true);
    $questionnaire = $responseArray[0];
    $this->assertSame([
        'id',
        'name',
        'email',
        'created_at',
        'updated_at'
    ], array_keys($questionnaire));
}

PHPの組み込みメソッドjson_decodeを使ってJSON→配列の変換を行います。

json_decodeの第二引数にtrueを渡すと連想配列に、falseを渡すとオブジェクトに変換します。

最後に

僕はこれまで業務で書いてきたテストコードの中で「こんな風に書いたらテストできるよ!」というのをまとめました。

PHPUnitでのテストコードの超基礎的なところをハンズオン形式で学べる記事を書いていますので、こちらも読んでみてください。

Laravelの学習にオススメの書籍・教材はこちらにまとめています。

最後に余談ですが、Laravelでテストコードを書く時に使うテストフレームワークは現状PHPUnit一択ですが、Pestというフレームワークもあるようです。

Udemyのオススメ講座はこちら↓

TypeScript+Next.js+Laravelハンズオンはこちら↓

デスク周りのオススメアイテムはこちら↓

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

ブログを開設するなら「SWELL」が絶対オススメ!

この記事を書いた人

大学院(機械工学)→重工業→エンジニア→プロダクトマネージャー(PdM)兼エンジニア

神戸で「つながる勉強会」を運営↓
https://tsunagaru-kobe.connpass.com/

神戸グルメのインスタアカウントを運用しています。

目次