前回の記事では、LaravelプロジェクトのPHPUnitテストをプラグインを使ってPestへ自動変換しましたが、今回は自分でPestのテストを作成します。外部APIのモック、基本的なアサーションや例外が投げられたパターンなどのテストです。

テスト対象

以下が今回のテスト対象であるTestClassです。getTranslate()では引数のテキストをAPIを使って翻訳し、結果をJsonレスポンスで返却。処理に失敗した場合は例外をスローするようになっています。

ラップしているTranslationApiは外部APIなので、今回はこちらはモックとします。

class TestClass
{
    public function __construct(
        private readonly TranslationApi $translator //これをモックする
    ) {}

    public function getTranslate(string $content): JsonResponse
    {
        try {
            $translatedContent = $this->translator->translate(
                $content
            );

            return response()->json([
                'status'  => 'success',
                'message' => '操作が成功しました。',
                'data'    => $translatedContent,
            ]);

        } catch (Exception $e) {

            throw new Exception('翻訳に失敗しました: '.$e->getMessage());
        }
    }
}

Pestの構文

Pestの構文は以下のようになっており、テストをグルーピングするdescribe()メソッドと、1つ1つのテストケースを指すtest()もしくはit()メソッドで構成されています。PHPUnitで書かれているクラス宣言は不要です。

describe('グループ名', function () {
   test('テスト1', function () {
       //テスト内容
    });
 
   it('テスト2', function () {
       //テスト内容
    });
});

describe()の使用は任意なので、最小構成のtest()it()のみで問題ありません。

またtest()it()のどちらを使うのかという点ですが、テストの動作自体は同じなのでどちらでも大丈夫です。ただit()を使った場合、実行結果の出力で以下のようにテスト名の前にitがつくようになっています。

共通処理

ではテストを書いてゆきます。今回は成功・失敗の2パターンテストを書きますので、共通の処理をbeforeEach()を使ってまとめておきます。beforeEach()は各テストケースの実行前に呼び出される、PHPUnitのsetUp()と同じような役割です。

モック対象であるTranslationApiのモック化と、テスト対象クラスのインスタンス化の2点を以下のように共通処理としました。

beforeEach(function () {
    // モックを作成
    $this->mock = Mockery::mock(TranslationApi::class);

    // テスト対象のクラスをインスタンス化
    $this->service = new testClass($this->mock);
});

モックにはPHPUnitと同様にMockeryを使用します。ここではmock()メソッドで対象クラスをモック化するのみとして、モックの振る舞いは各テスト内でセットします。

testClassクラスのインスタンス化も、PHPUnitと記述は特に変わりません。

なお$this->mock$this->serviceのプロパティ宣言は、Pestでは不要です。

成功パターンのテスト

「翻訳が成功して期待するレスポンスが返ってきたケース」のテストコードは以下になります。

test('翻訳 成功', function () {
    //準備
    $content = 'こんにちは';
    $expected = 'Hello';

    $this->mock
        ->shouldReceive('translate')
        ->once()
        ->with($content)
        ->andReturn($expected);

    //テスト実行
    $response = $this->testClass->getTranslate($content);

    expect($response->getData(true))
        ->toMatchArray([
            'status'  => 'success',
            'message' => '操作が成功しました。',
            'data'    => 'Hello',
        ]);
});

テストコードは大きく前半の準備部分と、後半の実行・アサーションの部分に分かれています。

まず前半では、モックの振る舞いを定義しています。shouldReceive('translate')は、translateメソッドが呼ばれること、once()は、呼ばれるのは1度だけという回数を、with($content)で渡される引数を、そして最後にandReturn()で、戻り値を指定しています。

次に後半の、テスト実行と戻り値のアサーションです。

受け取った$response自体はJsonResponseオブジェクトなので、$response->getData(true)でテストに必要な値だけを取り出しています。getData()はデフォルトでstdClassオブジェクトを返すので、ここでは引数にtrueを渡すことで配列として戻り値を受け取っています。

そして検証にはexpect()というPestのアサーションメソッドを使用します。expect()はそれ単体で使用することはなく、通常他のアサーション関数をチェーンします。今回はtoMatchArray()という配列比較の関数を繋ぎました。

toMatchArray()は上のテストコードのように全ての項目を比較することもできますし、以下のように、一部の項目だけといった部分一致も検証できるのでとても便利です。

    expect($response->getData(true))
        ->toMatchArray([
            'message' => '操作が成功しました。',
            'data'    => 'Hello',
        ]);

    expect($response->getData(true))
        ->toMatchArray([
            'data'    => 'Hello',
        ]);

また以下は配列比較でテストが失敗した場合のスクリーンショットですが、差分をこのようにわかりやすく出力してくれます。

失敗パターンのテスト

では次は、「翻訳が失敗し、例外を返すパターン」のテストコードです。

test('翻訳 失敗で例外が返る', function () {

    $content = 'こんにちは';
    $errorMessage = 'API error occurred';

    $this->mock
        ->shouldReceive('translate')
        ->once()
        ->with($content)
        ->andThrow(new Exception($errorMessage));

    $this->testClass->getTranslate($content);

})->throws(Exception::class, '翻訳に失敗しました: API error occurred');

先ほどの成功のテストとは少し構成が変わりました。

まずは前半のモック定義部分。ここは先ほどとほとんど同じですが、モックの戻り値の指定にandThrow(new Exception($errorMessage))として、例外を投げるように指定しています。

そして検証部分はチェーンで繋いだthrows()メソッドで行います。throws()はPestが提供しているメソッドで、第一引数に期待する例外クラスを、第二引数では例外のメッセージを渡します。

PHPUnitの場合、例外の検証時は以下のようにテストコードの冒頭に置かなければいけません。一方Pestではテスト実行後の最後にアサーションを配置できるので、テスト実行の流れと合っていてとても読みやすいと感じました。

public function test_翻訳失敗_PHPUnitの場合()
{
    $this->expectException(Exception::class);
    $this->expectExceptionMessage('翻訳に失敗しました: API error occurred');
    // ... 以下でテスト実行
}

関連記事

現在LaravelでPHPUnitを使っていてPestを導入しようかなと考えている方は、よろしければ以下の移行手順もご覧ください。

LaravelのPHPUnitテストをpest-plugin-driftでPestへ変換

メルマガ購読の申し込みはこちらから。

By hmatsu