メソッド内の変数をアサートするためのアプローチ

テストコードを使ってアサートしたくても、手の届きにくい実装を見たことがありませんか?

例として、メソッド内で作り出された変数が下記に当てはまる場合はどうでしょうか?

  • 戻り値として return されていない。
  • データベースに記録されていない。
  • 依存している外部APIに送信している。
  • だけど、テストコードに合わせるために現状の実装を大きく変えることは難しい。

例をあげるとこんな実装です。

class MyService {
    public function execute(
        PrimaryData $primaryData,
        SecondaryData $secondaryData,
    ) {
        $status = null;

        // 条件によって送信する値が変動する
        if ($secondaryData->enabled && $primaryData->opened) {
            $status  = true;
        }

        $params = [
            'name'  => $primaryData->name,
            'status' => $status,
            'company_name' => $primaryData->companyName,
            'area' => $secondaryData->area,
        ];

        // 外部に送信される値をテストでアサートしたい!

		$response = Http::timeout(seconds: 10)
			->withBody(
				content: $params,
				contentType: 'application/json; charset=UTF-8',
			)
			->post(url: 'https://external.example.com/api/path/to');

        return $response;
    }
}

こんな場合でも、実装にすこしの追記をするだけでテストできますよ!

答えから書きます

class  {
    public function execute(PrimaryData $primaryData, SecondaryData $secondaryData)
    {
        $status = null;

        if ($secondaryData->enabled && $primaryData->opened) {
            $status  = true;
        }

        $params = [
            'name'  => $primaryData->name,
            'status' => $status,
            'company_name' => $primaryData->companyName,
            'area' => $secondaryData->area,
        ];

        // 👇 これを追記します
        app(ProbeService)->execute(
            $params,
        );

		$response = Http::timeout(seconds: 10)
			->withBody(
				content: $params,
				contentType: 'application/json; charset=UTF-8',
			)
			->post(url: 'https://external.example.com/api/path/to');

        return $response;
    }
}

ProbeServiceの中身

namespace App\Services\TestProbe;

class ProbeService
{
    public function execute(mixed $value): void
    {
    }
}

魔法の準備が整いました。これだけです!メソッドの中身はありません、そしてなにも return しません。不思議ですね!

そしてこれがテストです

public function test外部APIに送信するパラメータは正しく組み立てられていること()
{
    // ProbeService モック化
    $mock = Mockery::mock(ProbeService::class);

    $mock
        ->shouldReceive('execute')
        ->once()
        ->with(
            Mockery::on(function ($params) {
                // 外部 API に送信されようとしている値をアサート
                $this->assertSame(
                    expected: [
                        'name'         => 'テスト タロウ',
                        'status'       => true,
                        'company_name' => 'テストカンパニー',
                        'area'         => '鳥取',
                    ],
                    actual: $params,
                );

                return true;
            }),
        );

    // ProbeService をモックに置き換え
    $this
        ->app
        ->instance(
            abstract: ProbeService::class,
            instance: $mock
        );

    // テスト対象の API にリクエストする
    $response = $this->post(
        'api/my-api/some-end-point',
        [
            'primary_data'   => [
                'name'         => 'テスト タロウ',
                'enabled'      => true,
                'company_name' => 'テストカンパニー',
            ],
            'secondary_data' => [
                'opened' => true,
                'area'   => '鳥取',
            ],
        ],
    );

    // 正常レスポンスが返ってきていること
    $response->assertOk();
}

モック化する部分が少し長いですが、暗記する必要はありません。コピペで良いです。