8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-19

はじめに

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

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

TL;DR

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

前提

環境

  • PHP 8.0.5
  • Laravel 8.5.0

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

Model

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

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

Controller

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

/**
 * ユーザーに紐づく注文の取得.
 *
 * @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は多重配列構造。

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

当初のテスト

テストする項目

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

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

assertAuthenticatedAs

  • API呼び出したユーザーは認証済みか

assertOk

  • レスポンスのステータスコードは200か

assertJsonCount

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

assertJsonStructure

  • 返却されたJsonの構造は正しいか

assertJson(AssertableJson)

  • whereAllType:取得したキーの型は正しいか
  • for文内のwhereAllType:ordersの配列内にあるキーの型は正しいか

assertExactJson

  • 取得値と想定値は完全一致(※)しているか
  • ※但し、注意点あり(文末に記載)

テストコード

/**
 * ユーザーに紐づく注文の取得のテスト.
 *
 * @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を使わなくてはいけなかった。

    • 理由は以下記事で。

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

修正後

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

テストする項目

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

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内で値の一致も確認できるので、不要

テストコード

/**
 * ユーザーに紐づく注文の取得のテスト.
 *
 * @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は第2引数にtrueを置くことで、厳格な一致(===)でテストできるので、これは積極的に使っていきたい

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

  • assertExactJsonで完全一致をテストできますが、内部を見ていくとPHPunitのassertEqualメソッドを使っており、厳格な一致をみているわけではないので、ここは留意しておく必要アリです。

    • 上述のassertJsonのようにオプションで厳格な一致、とかはできなさそう…
    • (2022/4/3更新)なんだか第2引数にtrueを渡すと厳格な一致を確認できそう!
      • 特に第2引数についての言及はないですが…

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

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

参考

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

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?