前回の記事でLaravelのassertJson
というアサーションの挙動ついて紹介しました。個人的に好ましい挙動ではないので、今回はassertJson
を使わずにJSONレスポンスの検証を行う方法について考えます。
なお調査と検証に使用した環境は以下です。
- PHP 7.4
- Laravel 8.x
結論
最初に結論を述べてしまうと
assertExactJson
を使うassertExactJson
が使えない場合、独自アサーションを使う
ということになります。
1. assertExactJsonを使う
LaravelにはJSONが完全に一致しているかを検証するassertExactJson
というアサーションも存在します。こちらを使えば、JSONとして完全に一致していることが保証されるので確実です。
2. assertExactJsonが使えない場合、独自アサーションを使う
ということで、基本的にassertExactJson
を使えば問題ないです。しかし、処理が実行時刻や乱数に依存している場合はassertExactJson
は使えない場合があります。
assertExactJsonが使えない例
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/users', function (Request $request) {
return response()->json([
'status' => 'OK',
'users' => [
[
'id' => 1,
'created_at' => '2020-12-01 00:00:01',
],
[
'id' => 2,
'created_at' => '2020-12-02 00:00:01',
],
[
'id' => 3,
'created_at' => date('Y-m-d H:i:s'),
],
],
]);
});
上記のusers APIでは users.3.created_at
が実行時刻依存になっていて固定が難しいです(この例は露骨ですが、残念ながらこれに近いものは世の中に存在します……)。こうなるとassertExactJson
は使えません。
このAPIの検証を既存のアサーションで実現しようとすると、
assertJson
でusers.3.created_at
以外を検証users.3.created_at
だけ個別に確認(存在 or 型など)
といった形になるのではないでしょうか。それは煩雑ですし、やがてリファクタリングをしてレスポンスの固定が可能となり、assertExactJson
へ移行した際に、assertJson
起因で別の問題が生じる可能性があるのでやっかいです。
独自アサーション
以上で見たように、assertExactJson
が使えない場合、Laravel側で用意されているアサーションでは検証が不便と感じたので、独自のアサーションを実装してみました。特徴は以下の通りです。
- 明示した部分では、型のみの検証にするなど、柔軟に対応できるようにした
- 利便性や
assertExactJson
へ自然な移行を考えて、引数をarray $data
のみとした
実装
Illuminate\Testing\TestResponse
を拡張する形で、レスポンス検証用の独自アサーションを実装します。
<?php
namespace Tests;
use Illuminate\Support\Arr;
use Illuminate\Testing\TestResponse as IlluminateTestResponse;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\Assert as PHPUnit;
class TestResponse extends IlluminateTestResponse
{
/**
* Assert that the response has the given JSON considering PHPUnit constraints.
*
* @param array $data
* @return $this
*/
public function assertExactJsonPartially(array $data)
{
foreach (Arr::dot($data) as $key => $expected) {
if (!$expected instanceof Constraint) {
continue;
}
if (!Arr::has($this, $key)) {
PHPUnit::fail("This response does not have the key '{$key}'");
}
$actual = Arr::get($this, $key);
PHPUnit::assertThat($actual, $expected, "Failed asserting for '{$key}'");
// Replace to actual value for assertExactJson
Arr::set($data, $key, $actual);
}
return $this->assertExactJson($data);
}
}
assertExactJson
をベースに、PHPUnitのConstraintを指定した部分では、指定したConstraintに応じた検証を行い、値が一致するかの検証は(事実上)行わないようにしました。例えば、PHPUnit\Framework\Constraint\IsType
を渡せば、型チェックのみとなりますし、PHPUnit\Framework\Constraint\IsAnything
を渡せば、存在さえしていれば何でも良いことになります。
TestResponseの拡張ができたら、テストクラス側でcreateTestResponse
をオーバーライドして、拡張したTestResponseクラスを生成するようにします。
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
/**
* Create the test response instance from the given response.
*
* @param \Illuminate\Http\Response $response
* @return TestResponse
*/
protected function createTestResponse($response)
{
return TestResponse::fromBaseResponse($response);
}
}
試してみる
先のusers APIに対してのテストとして、作成したアサーションを使ってみます。
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ApiTest extends TestCase
{
/**
* @test
*
* @return void
*/
public function api_users(): void
{
$expected = [
'status' => 'OK',
'users' => [
[
'id' => 1,
'created_at' => '2020-12-01 00:00:01',
],
[
'id' => 2,
'created_at' => '2020-12-02 00:00:01',
],
[
'id' => 3,
'created_at' => $this->isType('string'),
],
],
];
$this->getJson('/api/users')
->assertExactJsonPartially($expected);
}
}
IsType
を用いて id = 3のuserのcreated_atは文字列であれば何でもOKにしてみました。
実行してみると、無事にテストが通ります。
$ ./vendor/bin/phpunit tests/Feature/ApiTest.php
PHPUnit 9.4.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.068, Memory: 18.00 MB
OK (1 test, 2 assertions)
まとめ
assertJson
を使わずにJSONレスポンスの検証を行う方法を考えました。アサーションを独自に実装することで、assertExactJson
が使えない状況でも、ある程度詳細な検証が可能になり、安心してリファクタリングに取り組めるようになったかなと思います。