menu-icon

LaravelのassertJsonは使いたくない

LaravelにはJSON APIテスト用にassertJsonというアサートが存在します。このアサートは意図しない挙動をすることがあるので、使わない方が無難だと思った話です。

概要

assertJsonはJSON APIのレスポンスをテストするためのアサートです。Laravel 5.4で登場し、2020年11月現在の最新バージョンである8.xにも存在します。公式ドキュメントには次のような記載があります。

Tip!! The assertJsonメソッドはレスポンスを配列へ変換し、PHPUnit::assertArraySubsetを使用しアプリケーションへ戻ってきたJSONレスポンスの中に、指定された配列が含まれているかを確認します。そのため、JSONレスポンスの中に他のプロパティが存在していても、このテストは指定した一部が残っている限り、テストはパスし続けます。

https://readouble.com/laravel/8.x/ja/http-tests.html#assert-json

これだけ見ると、「レスポンス内で検証したい部分だけ検証できて便利ですね」と思ってしまいますが、そうではありません。挙動をきちんと理解していないと、思わぬ落とし穴にハマります。

問題点

今回assertJsonの問題点を2点取り上げます。説明のため、以下のような固定値を返すAPI(/api/users)に対するテストを考えます。

routes/api.php
<?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を使わずに検証する方法について考えます。