📝

Laravelのjsonに対するテストのetcメソッド

2023/12/12に公開

https://readouble.com/laravel/9.x/ja/http-tests.html#fluent-json-testing

// api/sampleで下記を返すとする
return response()->json([
	'sample' => true,
	'sampleLevel1' => [
	    'name' => 'aaa',
	    'number' => 1,
	],
]);

このようなルートがあるとしてこのルートへのtestを書く時のetcメソッドについて比較してみた。

public function testJsonEtc(): void
{
	$response = $this->get('api/sample');
	$response->assertJson(
	    fn (AssertableJson $json) =>
	    $json
		->where('sample', true)
	);
}

etc無し。

  FAIL  Tests\Feature\Api\SampleTest
  ⨯ json etc

  ---

  • Tests\Feature\Api\SampleTest > json etc
  Unexpected properties were found on the root level.
  Failed asserting that two arrays are identical.

  at tests/Feature/Api/SampleTest.php:20
     16$response->assertJson(
     17▕             fn (AssertableJson $json) =>
     18$json
     19▕                 ->where('sample', true)20);
     21}
     22}
     23▕ 
  --- Expected
  +++ Actual
  @@ @@
  -Array &0 ()
  +Array &0 (
  +    1 => 'sampleLevel1'
  +)

  Tests:  1 failed
  Time:   0.65s

当然NGとなる

public function testJsonEtc(): void
{
  $response = $this->get('api/sample');
  $response->assertJson(
      fn (AssertableJson $json) =>
      $json
  	->where('sample', true)
+	->etc()
  );
}

etcをトップに追加

PASS  Tests\Feature\Api\SampleTest
✓ json etc

Tests:  1 passed
Time:   0.65s

通る

public function testJsonEtc(): void
{
    $response = $this->get('api/sample');
    $response->assertJson(
        fn (AssertableJson $json) =>
        $json
            ->where('sample', true)
+           ->has('sampleLevel1')
+           ->where('sampleLevel1.name', 'aaa')
            ->etc()
    );
}

Level1のname属性のアサート追加

   PASS  Tests\Feature\Api\SampleTest
  ✓ json etc

  Tests:  1 passed
  Time:   0.59s

一応通る

public function testJsonEtc(): void
{
    $response = $this->get('api/sample');
    $response->assertJson(
        fn (AssertableJson $json) =>
        $json
            ->where('sample', true)
            ->has('sampleLevel1')
            ->where('sampleLevel1.name', 'aaa')
-           ->etc()
   );
}

etcメソッドを消す。
期待しているのはテストが失敗すること

  • sampleLevel1.numberを無視しているから
   PASS  Tests\Feature\Api\SampleTest
  ✓ json etc

  Tests:  1 passed
  Time:   0.66s

もちろん通る。

etcメソッドを理解する
上記の例では、アサートのチェーンの最後でetcメソッドを呼び出したことに気づかれた方もいらっしゃるでしょう。このメソッドはLaravelへJSONオブジェクト中に他の属性が存在する可能性があることを伝えます。etcメソッドが使用されていない場合は、JSONオブジェクトに他の属性が存在していることをアサートしていないため、テストは失敗します。
この動作の意図は、属性に対して明示的にアサートを行うか、etc メソッドで追加の属性を明示的に許可することで、JSONレスポンスで意図せず機密情報を公開してしまうことを防ぐことにあります。
しかし、etcメソッドをアサートのチェーンに含めないからといっても、JSONオブジェクトのネストに含まれる配列へ、追加の属性が追加されないわけではないことを認識しておく必要はあります。etcメソッドは、etcメソッドを呼び出したネストレベルで、追加の属性が存在しないことを保証するだけです。

etcメソッドを呼び出したネストレベルで実行されるからsampleLevel1と同階層で呼び出したらsampleLevel1の中身は考慮しない。
期待しているテストが失敗するコードは

$response = $this->get('api/sample');
$response->assertJson(
        fn (AssertableJson $json) =>
        $json
            ->where('sample', true)
            ->has(
                'sampleLevel1',
                fn (AssertableJson $json) =>
                $json
                    ->where('name', 'aaa')
            )
);
  • Tests\Feature\Api\SampleTest > json etc
  Unexpected properties were found in scope [sampleLevel1].
  Failed asserting that two arrays are identical.

  at tests/Feature/Api/SampleTest.php:22
     18'sampleLevel1',
     19▕                     fn (AssertableJson $json) =>
     20$json
     21▕                         ->where('name', 'aaa')22)
     23);
     24}
     25}
     261   tests/Feature/Api/SampleTest.php:23
      Illuminate\Testing\TestResponse::assertJson()
  --- Expected
  +++ Actual
  @@ @@
  -Array &0 ()
  +Array &0 (
  +    1 => 'number'
  +)

  Tests:  1 failed
  Time:   0.65s

number属性を何もしてないから失敗するが、これが期待している

class SampleTest extends TestCase
{
    public function testJsonEtc(): void
    {
        $response = $this->get('api/sample');
        $response->assertJson(
            fn (AssertableJson $json) =>
            $json
                ->where('sample', true)
                ->has(
                    'sampleLevel1',
                    fn (AssertableJson $json) =>
                    $json
                        ->where('name', 'aaa')
+                       ->where('number', 1)
                )
        );
    }
}
class SampleTest extends TestCase
{
    public function testJsonEtc(): void
    {
        $response = $this->get('api/sample');
        $response->assertJson(
            fn (AssertableJson $json) =>
            $json
                ->where('sample', true)
                ->has(
                    'sampleLevel1',
                    fn (AssertableJson $json) =>
                    $json
                        ->where('name', 'aaa')
+                       ->etc()
                )
        );
    }
}

etcのネストレベルに注意しないと無いことを許容したい属性やテストし忘れてる属性が漏れてもったいない

Discussion