LaravelにはJSON APIテスト用にassertJson
というアサートが存在します。このアサートは意図しない挙動をすることがあるので、使わない方が無難だと思った話です。
概要
assertJson
はJSON APIのレスポンスをテストするためのアサートです。Laravel 5.4で登場し、2020年11月現在の最新バージョンである8.xにも存在します。公式ドキュメントには次のような記載があります。
Tip!! The
https://readouble.com/laravel/8.x/ja/http-tests.html#assert-jsonassertJson
メソッドはレスポンスを配列へ変換し、PHPUnit::assertArraySubset
を使用しアプリケーションへ戻ってきたJSONレスポンスの中に、指定された配列が含まれているかを確認します。そのため、JSONレスポンスの中に他のプロパティが存在していても、このテストは指定した一部が残っている限り、テストはパスし続けます。
これだけ見ると、「レスポンス内で検証したい部分だけ検証できて便利ですね」と思ってしまいますが、そうではありません。挙動をきちんと理解していないと、思わぬ落とし穴にハマります。
問題点
今回assertJson
の問題点を2点取り上げます。説明のため、以下のような固定値を返すAPI(/api/users
)に対するテストを考えます。
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/users', function (Request $request) {
return response()->json([
['id' => 1, 'name' => 'hoge'],
['id' => 2, 'name' => 'fuga'],
['id' => 3, 'name' => 'piyo'],
]);
});
問題1. 添字配列の部分一致がわかりづらい
1つ目の問題は、添字配列の部分一致を検証する際の挙動です。
配列内に特定の要素が存在することを検証したいことがあると思います。例えば、{"id":1,"name":"hoge"}
が存在することを検証する場合、次のように書けます。
/**
* @test
*
* @return void
*/
public function containHoge(): void
{
$expected = [
['id' => 1, 'name' => 'hoge'],
];
$this->getJson('/api/users')
->assertJson($expected, true);
}
これは意図通りにパスします。
PHPUnit 9.4.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.148, Memory: 18.00 MB
OK (1 test, 1 assertion)
では、{"id":2,"name":"fuga"}
の場合はどうでしょうか。
/**
* @test
*
* @return void
*/
public function containFuga_NG(): void
{
$expected = [
['id' => 2, 'name' => 'fuga'],
];
$this->getJson('/api/users')
->assertJson($expected, true);
}
「指定した配列が含まれているか」だと、これで問題ないように見えます。しかし、このテストは通りません。
PHPUnit 9.4.2 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 00:00.134, Memory: 20.00 MB
There was 1 failure:
1) Tests\Feature\ApiUsersTest::containFuga_NG
Unable to find JSON:
(中略)
--- Expected
+++ Actual
@@ @@
array (
0 =>
array (
- 'id' => 2,
- 'name' => 'fuga',
+ 'id' => 1,
+ 'name' => 'hoge',
),
1 =>
array (
/var/www/html/vendor/laravel/framework/src/Illuminate/Testing/Constraints/ArraySubset.php:85
/var/www/html/vendor/laravel/framework/src/Illuminate/Testing/Assert.php:49
/var/www/html/vendor/laravel/framework/src/Illuminate/Testing/AssertableJsonString.php:270
/var/www/html/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:516
/var/www/html/tests/Feature/ApiUsersTest.php:54
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
上のメッセージを読むとわかりますが、assertArraySubset
の仕様上、以下のように添字を合わせなければなりません。
/**
* @test
*
* @return void
*/
public function containFuga_OK(): void
{
$expected = [
1 => ['id' => 2, 'name' => 'fuga'],
];
$this->getJson('/api/users')
->assertJson($expected, true);
}
これで意図した検証が可能になります。
PHPUnit 9.4.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.147, Memory: 18.00 MB
OK (1 test, 1 assertion)
問題2. 空配列が検証できない
2つ目の問題は空配列の検証ができないことです。
レスポンスが空配列であることを検証したい場合を考えます。
/**
* @test
*
* @return void
*/
public function empty(): void
{
$expected = [];
$this->getJson('/api/users')
->assertJson($expected, true);
}
一見、これで空配列が返ることを検証できそうです。しかし、そうではありません。/api/users
のレスポンスを考えれば、このテストは失敗するはずですが、パスしてしまうのです。
PHPUnit 9.4.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.130, Memory: 18.00 MB
OK (1 test, 1 assertion)
assertArraySubset
の仕様上、空配列を指定すると、どのような配列が返ってきてもテストをパスしてしまいます。つまりassertJson
では事実上、空配列は検証できません。
まとめ
LaravelのassertJson
の問題点を2点紹介しました。どちらもPHPUnitのassertArraySubset
が起因となっています。実はこのアサート、混乱を招くということで、PHPUnit 8で非推奨、9で削除となりました。なので、それを使用しているassertJson
が分かりづらいのも当然ですね(PHPUnit 9を使っている場合、Laravel側で複製したassertArraySubset
が呼ばれます)。
今回はassertJson
を使いたくない理由を示しましたので、次回はassertJson
を使わずに検証する方法について考えます。