phpunitでテストコードを書く時に使うテンプレ＆ネタ

最近は、Laravelで開発されたアプリケーションのテストコードをphpunitで書いています。
今回は、テストをするときに使いまわせる部分だけ抜き出してまとめてみました。


## 1.リクエストクラスのバリデーションをテストしたい

キーワード：request, rules(), dataProvider, データプロバイダ

■ポイント
・データプロバイダを使うと複数のパラメータパターンのテストが書きやすい＆見やすい
・テストを実行するメソッドのアノテーションに`@dataProvider` を記載する。
・テストケース名は「どのような値か 型 成功(success)or失敗(error)」でつけると分かりやすい。

#### テンプレ

```.php
...
use App\Http\Requests\MyProfile\UserRegisterRequest;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
...(略)

   /**
    * @test
    * @dataProvider dataproviderValidation
    */
    public function validationCheck(array $params, bool $expected): void
    {
        $request = new UserRegisterRequest();
        $rules = $request->rules();
        $validator = Validator::make($params, $rules);
        $result = $validator->passes();
        $this->assertEquals($expect, $result);
    }

    /**
    * バリデーションチェック用データ
    */
    public function dataproviderValidation()
    {
        return [
            'email string success' => [
                [
                    'email' => "test@t.com",
                ],
                true,
            ],
            'email null error' => [
                [
                    'email' => null,
                ],
                false,
            ],
        ];
    }
```

#### 説明あり（理解用）

```.php
...
use App\Http\Requests\MyProfile\UserRegisterRequest;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
...(略)

   /**
    * @test // テスト対象であることを示すアノテーション
    * @dataProvider {データを返すメソッド} // このアノテーションは必須
    */
    public function validationCheck(array $params, bool $expected): void
    {
        $request = new {テスト対象のリクエストクラス};
        $rules = $request->rules();
        $validator = Validator::make($params, $rules);
        $result = $validator->passes(); // true/falseが返る
        $this->assertEquals($expect, $result);
    }

    /**
    * バリデーションチェック用データ
    */
    public function dataproviderValidation()
    {
        return [
            '{テストケース名}' => [ // ケース失敗時にどのケースか分かりやすくなる
                [
                    '{パラメータ名}' => '{パラメータ}',
                ],
                {結果},
            ],
            'email null error' => [
                [
                    '{パラメータ名}' => '{パラメータ}',
                ],
                {結果},
            ],
        ];
    }
```

参考：
・2. PHPUnit 用のテストの書き方
https://phpunit.readthedocs.io/ja/latest/writing-tests-for-phpunit.html#writing-tests-for-phpunit-data-providers

・【Laravel】フォームリクエストバリデーションのテストコード作成
https://qiita.com/n_mogi/items/57a946205df2a69889c2



## 2.リクエストクラスのバリデーションメッセージもテストしたい
キーワード：request, rules(), dataProvider, データプロバイダ, message()

■ポイント
・関数の引数に `array $messages` を追加。`setCustomMessages()` でリクエストのmessages()を呼び出す。
・assertSameでメッセージ内容をテスト。

#### テンプレ

```.php
...
use App\Http\Requests\MyProfile\UserRegisterRequest;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
...(略)

   /**
    * @test
    * @dataProvider dataproviderValidation
    */
    public function validationCheck(array $params, array $messages, bool $expected): void
    {
        $request = new UserRegisterRequest();
        $rules = $request->rules();
        $validator = Validator::make($params, $rules);
        $validator = $validator->setCustomMessages($request->messages()); // 追記
        $result = $validator->passes();
        $this->assertEquals($expect, $result);
        $this->assertSame($messages, $validator->errors()->messages()); // 追記。メッセージをassert。
    }

    /**
    * バリデーションチェック用データ
    */
    public function dataproviderValidation()
    {
        return [
            'email null' => [
                [
                    'email' => null
                ],
                [ // メッセージのパラメータ配列を追加。パラメータの「array $messages」に対応。
                    'email' => [
                        'Eメールを入力してください。',
                    ],
                ],
                 true
            ],
        ];
    }
```


## 3.リクエストクラスのwithValidator()もテストしたい
キーワード：request, rules(), dataProvider, データプロバイダ, withValidator()

■ポイント
・`$request->withValidator($validator);` を追記する。

#### テンプレ

```.php
...
use App\Http\Requests\MyProfile\UserRegisterRequest;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
...(略)

   /**
    * @test
    * @dataProvider dataproviderValidation
    */
    public function validationCheck(array $params, bool $expected): void
    {
        $request = new UserRegisterRequest();
        $rules = $request->rules();
        $validator = Validator::make($params, $rules);
        $request->withValidator($validator); // 追記
        $result = $validator->passes();
        $this->assertEquals($expect, $result);
    }

    /**
    * バリデーションチェック用データ
    */
    public function dataproviderValidation()
    {
        return [
            'email string success' => [
                [
                    'email' => "test@t.com",
                ],
                true,
            ],
            'email null error' => [
                [
                    'email' => null,
                ],
                false,
            ],
        ];
    }
```

## 4.ログ出力をテストしたい
キーワード：log, 'logging.channels.stderr.level', getLogger(), popProcessor()

■ポイント
・setup()で現在のログレベル設定を取得。tearDown()で元の設定に戻す。
・各テストケースの冒頭でログレベルを'debug'に設定する。

#### テンプレ
```.php
...
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
...(略)

    private string $logLevel;

    protected function setUp(): void
    {
        parent::setUp();
        $this->testTarget = new TestTargetLog();
        $this->logLevel= Config::get('logging.channels.stderr.level');
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        Config::set('logging.channels.stderr.level', $this->logLevel);
    }

    /**
     * @test
     * 【正常系】操作ログ：メッセージのログ出力を確認
     */
    public function operationSuccess()
    {
        Config::set('logging.channels.stderr.level', 'debug');

        $content = [];
        $logMessage = "logTest Message";
        Log::getLogger()->pushProcessor(function ($record) use (&$content) {
            $content[] = $record['message'];
            $content[] = $record['context'];
            return $record;
        });
        $this->testTarget->operation($logMessage);
        Log::getLogger()->popProcessor();

        $this->assertSame($content[0], $logMessage);
        $this->assertSame($content[1], ["operation" => true]);
    }
```

#### 説明あり（理解用）
```.php
...
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
...(略)

    private string $logLevel; // ログレベルバックアップ用変数

    protected function setUp(): void
    {
        parent::setUp();
        $this->testTarget = new TestTargetLog();
        $this->logLevel= Config::get('logging.channels.stderr.level'); // 現在のログレベルを取得
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        Config::set('logging.channels.stderr.level', $this->logLevel); // ログレベルを元に戻す
    }

    /**
     * @test
     * 【正常系】操作ログ：メッセージのログ出力を確認
     */
    public function operationSuccess()
    {
        Config::set('logging.channels.stderr.level', 'debug'); // ログレベルを'debug'に設定

        $content = []; // ログを入れるための配列
        $logMessage = "logTest Message";
        Log::getLogger()->pushProcessor(function ($record) use (&$content) { // ログ取得処理
            $content[] = $record['message'];
            $content[] = $record['context'];
            return $record;
        });
        {"テスト対象の処理"}
        Log::getLogger()->popProcessor(); // ログ取得終わり

        $this->assertSame($content[0], $logMessage);
        $this->assertSame($content[1], ["operation" => true]);
    }
```

## 5.protected, privateメソッドをテストしたい
キーワード：protected, private, プロテクト, プライベート, Reflection

■ポイント
・ReflectionClassを使う

#### テンプレ
```.php
    /**
     * @test
     */
    public function Validation(): void
    {
        $params= [1];
        $target = new TestTargetClass();
        $reflection = new \ReflectionClass($target);
        $method = $reflection->getMethod('getMessages');
        $method->setAccessible(true);
        $method->invokeArgs($target , [$params]);
    }
```

#### 説明有り（理解用）
```.php
    /**
     * @test
     */
    public function Validation(): void
    {
        $target = new {テスト対象のクラス}();
        $reflection = new \ReflectionClass($target);
        $method = $reflection->getMethod({テスト対象のprotectedメソッド});
        $method->setAccessible(true);
        $method->invokeArgs($target , [{メソッドに引数が必要な場合はここに書く}]);
    }
```

## 6.モック作成には2種類ある

キーワード：mock, createMock(), mockery, mock()

■ポイント
・「phpunitのcreateMock()」と「mockeryの\Mockery::mock()」の2種類
・mockeryはモックのフレームワーク
・mockeryの方ができることが多い(?)
　ex)mockeryはfinal, private, staticのモックが作成できる

#### テンプレ
```.php
// phpunitのモック
$sampleMock = $this->createMock(SomeClass::class);
$sampleMock->method('getNubmer')
    ->willReturn('123');

// Mockeryのモック
$sampleMock = \Mockery::mock('SomeClass');
$sampleMock->shouldReceive('getNubmer')
    ->andReturn('123');
```

ちなみに...
・mockeryはuseでエイリアスを記載しておけばシンプルに書ける。

```.php
use Mockery as m;
...
// Mockeryのモック
$sampleMock = m::mock('SomeClass');
```

>参考
8. テストダブル
https://phpunit.readthedocs.io/ja/latest/test-doubles.html

## 7.静的なメソッドをモックしたい
キーワード：静的, モック, mockery

■ポイント
・mockeryのaliasを使う。

#### テンプレ
```.php
$mockReplaceService = \Mockery::mock("alias:" . ReplaceServiceClass::class);
$mockReplaceService->shouldReceive("getMemberName")
        ->andReturn("taro");
```

## 8.テストをスキップしたいとき

キーワード：スキップ, skip, mark

■ポイント
・markTestSkipped()を使う。
・元ソースに間違いがある時等に使う
・メッセージは任意。
・結果が「S」と表示される。

#### テンプレ
```.php
    $this->markTestSkipped('ルール修正が必要.');
```

## 9.テストのログレベルを変更したいとき
キーワード：ログレベル

■ポイント
`.ent.testing` の `LOG_STDERR_LEVEL=critical` を変更する。
`error` に設定するとより詳細なログメッセージが確認できる。

## 10.同じメソッドが異なる引数で異なる返り値を返す場合のテストにはConsective()を使う
キーワード：同じメソッドが異なる引数で異なる返り値, Consective

異なる引数を指定するときには、withConsecutive()を使い、
異なる返り値を指定するときには、willReturnOnConsecutiveCalls()を使う。

```.php
■テスト対象ソース
$max = $this->counter->getCount(Config::get('const.count_max'));
$min = $this->counter->getCount(Config::get('const.count_min'));

■テストコード
$max = Config::get('const.count_max');
$min = Config::get('const.count_min');

/**
* getCount()が$max, $minを引数として2回呼ばれ、10,0が返り値となることをテストする。
*/
$counter->expects($this->exactly(2))
  ->method('getCount')
  ->withConsective([$max], [$min])
  ->willReturnOnConsectiveCalls(10, 0);
```

## 11.コントローラーのテスト
キーワード：コントローラー、Controller

``` .php
<?php

declare(strict_types=1);

namespace Tests\Feature\App\Http\Controllers\User;

use Tests\TestCase;

/**
 * コントローラーのテスト
 */
class UserControllerTest extends TestCase
{
    /**
     * @test
     * [正常]
     */
    public function index(): void
    {
        $uri = '{テスト対象のURL}';

        // interfaceを使用している場合
        $this->app->bind(UserUseCaseInterface::class, function () use ($UserUseCaseInterface) {
            return $UserUseCaseInterface;
        });

        $response = $this->get($uri);
        $response->assertStatus(200);
    }
}
```


## 最後に
他にもあれば随時更新していきます。