[Laravel]初めてのAPIテスト～一つのリファクタ案～

# はじめに

初めてのAPIのテストコードを書いてみて色々と学びが多かったので、備忘録。

同じようにAPIテストコードを書いている人、これから書く人の参考になれば幸いです。

# TL;DR

- Laravelであれば、取得値したJsonデータを配列に置換する必要なし
- 中でも**AssertableJsonが超便利**

# 前提

## 環境

- PHP 8.0.5
- Laravel 8.5.0

## テスト前の準備とテスト内容

### Model

まずはApp/Models/User.phpにてユーザーに紐づく注文を取得できるようにリレーションを張る

```php
/**
 * ユーザーに紐づく注文の取得.
 *
 * @return \Illuminate\Database\Eloquent\Relations\HasMany
 */
public function orders()
{
    return $this->hasMany(Order::class);
}
```

### Controller

Controllerのindexメソッドで返ってくるAPIのJSONデータをテスト

```php
/**
 * ユーザーに紐づく注文の取得.
 *
 * @return \Illuminate\Database\Eloquent\Relations\HasMany
 */
public function index()
{
	// ログインユーザーの注文の取得
    $orders = User::findOrFail(Auth::id())
        ->orders()
        ->get();

    return response()->json([
        // 準正常を判断するsuccess
        'success' => true,
        'orders' => $orders->map(function ($orders) {
            return [
                'uuid' => $orders->uuid,
                'name' => $orders->name,
                'phone_number' => $orders->phone_number,
                'updated_at' => $orders->updated_at->format('Y-m-d H:i:s.u'),
            ];
        }),
    ]);
}
```

以下のJsonデータが返ってくるAPIのテストをする

ordersは多重配列構造。

```php
{
    success : true,
    orders : 
    [
        uuid: ...
		name: ...
		phone_number: ...
		updated_at: ...
    ],
    [
        uuid: ...
		name: ...
		phone_number: ...
		updated_at: ...
    ],
	...
}
```

# 当初のテスト

## テストする項目

当初、API呼び出した後のテスト時に使ったメソッドとテストする内容は以下のようになっていた

※各メソッドについてはドキュメントみれば「ああ、こんな使い方するのね」ってなるので割愛

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

> assertAuthenticatedAs
- API呼び出したユーザーは認証済みか

> assertOk
- レスポンスのステータスコードは200か

> assertJsonCount
> 
- 返却されたJsonのキーの数は2個か
- 返却されたJsonの内、ordersにある配列のキーは4つか

> assertJsonStructure
> 
- 返却されたJsonの構造は正しいか

> assertJson（AssertableJson）
> 
- whereAllType：取得したキーの型は正しいか
- for文内のwhereAllType：ordersの配列内にあるキーの型は正しいか

> assertExactJson
> 
- 取得値と想定値は完全一致（※）しているか
- ※但し、注意点あり（文末に記載）

## テストコード

```php
/**
 * ユーザーに紐づく注文の取得のテスト.
 *
 * @void
 */
protected function testIndex()
{
	// ユーザー、注文情報の取得
	$user = User::findOrFail(1); // ここは仮でIDおいてます。
    $orders = $user->orders;

	// API呼び出し(routeの名前は仮)
    $response = $this->actingAs($user)->getJson(route('api.orders.index'));

	// jsonデータを配列に変換
    $contentArray = json_decode((string) $response->getContent(), true);
    $contentArrayCount = count($contentArray['orders']);

	// 以下ひたすらテスト（詳細は上記「テストする項目」参照）
    $this->assertAuthenticatedAs($user);
    $response->assertOk();
    $response->assertJsonCount(2);
    for ($i = 0; $i <= $contentArrayCount - 1; $i++) {
        $response->assertJsonCount(4, "orders.$i");
    }
    $response->assertJsonStructure([
        'success',
        'orders' => [
            '*' => [
                'uuid',
                'name',
                'phone_number',
                'updated_at',
            ],
        ],
    ]);
    $response->assertJson(function (AssertableJson $json) use ($contentArrayCount) {
        $json->whereAllType([
            'success' => 'boolean',
            'orders' => 'array',
        ]);
        for ($k = 0; $k <= $contentArrayCount - 1; $k++) {
            $json->has("orders.$k", function ($ordersJson) {
                $ordersJson->whereAllType([
                    'uuid' => 'string',
                    'name' => 'string',
                    'phone_number' => 'string',
                    'updated_at' => 'string',
                ]);
            });
        }
    }, true);
    $response->assertExactJson([
        'success' => true,
        'orders' => $this->orders->map(function ($order) {
            return [
                'uuid' => $order->uuid,
                'name' => $order->name,
                'phone_number' => $order->phone_number,
                'updated_at' => $order->updated_at->format('Y-m-d H:i:s.u'),
            ];
        })
        ->toArray(),
    ]);
}
```

作った後の感想としては以下課題が見つかった

- Jsonから配列に変換する処理が冗長なので消したい

- いろんなJson関連のテストメソッドを使っていて、各々なんのテストしているのかパッとわかりにくい

- Json内のキーの数確認（assertJsonCount）や構造確認（assertJsonStructure）、完全一致（assertExactJson）はテスト内容として重複しているようで、冗長な気がしていた

- ステータスコードのテストが文字だけのメソッド（`assertOk`など）になると逆にわかりづらい

- `AssertableJson`内でfor文使うのは汚い

- assertExactJsonでtoArrayを使わなくてはいけなかった。
  - 理由は以下記事で。

https://qiita.com/TkTkTkTkTako/items/23605f59fd1da1734158

これはどうにかならないかと、相談や調査してみた。

# 修正後

同僚のアドバイスももらいつつ、以下のように修正

## テストする項目

テストする項目から精査した

> assertAuthenticatedAs（変更なし）
> 
- API呼び出したユーザーは認証済みか

> assertOk→**assertStatus(200)**
> 
- レスポンスのステータスコードは200か
- **直感的にわかりにくいので、assertStatusを使用**

> ~~assertJsonCount~~→**削除**
> 
- ~~返却されたJsonのキーの数は2個か~~
- ~~返却されたJsonの内、ordersにある配列のキーは4つか~~
- **AssertableJson内のhasAllで十分テストできるので削除**

> ~~assertJsonStructure~~→**削除**
> 
- ~~返却されたJsonの構造は正しいか~~
- コチラ**もAssertableJson内のhasAll、whereAllTypeで十分テストできる**ので削除

> assertJson（AssertableJson）
> 
- whereAllType：取得したキーの型は正しいか
- **where, whereAllを使って値の一致もAssertableJson内でテスト**

> assertExactJson→**削除**
> 
- ~~for文内のwhereAllType：ordersの配列内にあるキーの型は正しいか~~
- **AssertableJson内で値の一致も確認できるので、不要**

## テストコード

```php
/**
 * ユーザーに紐づく注文の取得のテスト.
 *
 * @void
 */
public function testIndex()
{
	// ユーザー、注文情報の取得
	$user = User::findOrFail(1); // ここは仮でIDおいてます。
    $orders = $user->orders;

	// API呼び出し(routeの名前は仮)
    $response = $this->actingAs($user)->getJson(route('api.orders.index'));

	// 以下テスト
    $this->assertAuthenticatedAs($user);
    $response->assertStatus(200);
    $response->assertJson(function (AssertableJson $json) use ($orders) {
        $orderCount = $orders->count();

        // json内のキー有無、型確認
        $json->hasAll(['success', 'orders']);
        $json->whereAllType([
            'success' => 'boolean',
            'orders' => 'array',
        ]);
        // successの値の一致を確認
        $json->where('success', true);
        // json内のorders配列内のキー有無、型、値の一致を確認
        $ordersFirst = $orders->first();
        $json->has('orders', $orderCount, function ($json) use ($ordersFirst) {
            $json->hasAll('uuid', 'name', 'phone_number', 'updated_at')
			->whereAllType([
                'uuid' => 'string',
                'name' => 'string',
                'phone_number' => 'string',
                'updated_at' => 'string',
            ])->whereAll([
                'uuid' => $ordersFirst->uuid,
                'name' => $ordersFirst->name,
                'phone_number' => $ordersFirst->phone_number,
                'updated_at' => $ordersFirst->updated_at->format('Y-m-d H:i:s.u'),
            ]);
        });
    });
}
```

## 改善ポイント

上述した課題を全て解決

- Jsonから配列に変換する処理が冗長なので消したい
     →**AssertableJson内の書き方を修正**することで、配列に置換する必要なし

- いろんなJson関連のテストメソッドを使っていて、各々なんのテストしているのかパッとわかりにくい
    →ほとんどのテストを**AssertableJsonに置換**することで全体的にスマートになった

- Json内のキーの数確認（assertJsonCount）や構造確認（assertJsonStructure）、完全一致（assertExactJson）はテスト内容として重複しているようで、冗長な気がしていた
    →コチラも**AssertableJsonへの置換**で解決

- ステータスコードのテストが文字だけのメソッド（`assertOk`など）になると逆にわかりづらい
    →**assertStatus(200)への置換**で解決

- `AssertableJson`内でfor文使うのは汚い
    →コチラも**AssertableJsonの書き方の修正**でfor文を削除

賛否両論あるかもしれませんが、個人的には**ほとんどのテスト（キー有無／型／値のテスト）をAssertableJson内で完結させる**ことができたのはいい整理だな、と思い最終的にこの形にしました。

# おわりに

APIのテストコードを書いて思ったことをつらつらと。

- Laravelが用意しているjsonのテスト用のメソッドがあれば**取得したJSONの値を配列に変換して...みたいな作業がいらない**ので非常に便利

- さらに、**AssertableJsonを使えばキー、値、型についてなんでもテストできてしまう**のでマストで使いたい
    - ただ今回のように**jsonデータに多重配列構造が入っている際は、書き方が特別**なのでそこは要確認かなと思います。（上述の修正後のコードを参照）

- APIテストの際に必ず使う`assertJson`は第２引数にtrueを置くことで、**厳格な一致（===）でテストできる**ので、これは積極的に使っていきたい

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

- ステータスコードのテスト用にLaravelは`assertOk(200ステータス)`, `assertNotFound(404ステータス)`を用意しているけど、phpUnitが用意している`assertStatus(200)`, `assertStatus(404)`の方が直感的にわかりやすい（自分だけでしょうか…）

- assertExactJsonで完全一致をテストできますが、内部を見ていくとPHPunitの**assertEqualメソッドを使っており、厳格な一致をみているわけではない**ので、ここは留意しておく必要アリです。
    - ~~上述のassertJsonのようにオプションで厳格な一致、とかはできなさそう…~~
    - (2022/4/3更新)なんだか第２引数にtrueを渡すと厳格な一致を確認できそう！
        - 特に第２引数についての言及はないですが…

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

- これは一般的なテストコード書いてみての感想ですが、テストコードを書いた後だと普段の実装でテストを意識してコードを書くようになるので、普段書くコードの可読性が上がった気がする

今回はJsonのテストだけでしたが、LaravelのHTTPテストには他にも便利そうなテストメソッドがあるので、積極的に使っていくのがいいかと。

## 参考

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

最後までお読み頂きありがとうございました。