Laravel APIの自動テストを運用してみた話

Laravel APIの自動テストを運用してみた話

会社で作成中のサイクリストのためのSNS(HILCRA)のAPI開発において、特に自動テストに力を入れて取り組んでみたので、その内容について記録として残しておこうと思います。
ちなみに、開発のフレームワークはLaravel、テスティングフレームワークはPHPUnitです。
テスト観点の設計や、認証を含むAPIにおける具体的なテストコーディングの参考になれば幸いです。

そもそもなぜ自動テストが必要か

一言で言うと「手動テストでは網羅性に限界がある&時間がかかる」からだと思いますが、実際に運用してみて具体的には以下二点のメリットが大きいと感じました。

変更した機能以外への影響が即座にわかる

自分が変更した箇所の確認は普通念入りに行うと思いますが、やっかいなのは自分が変更した箇所と関係ないと思っている箇所で不具合が出るケース。かと言って、手動で全てのAPIに影響がないことを確認する時間はありませんよね。自動テストを書いておけば、他機能に影響が無いことに自信を持つことができます。

機能実装時の思考の幅・時間が増え品質が上がる

テストを書くためには、正常系、異常系のパターンを網羅的に考える必要があります。つまり単純に機能実装のみを行う時と比べて思考の幅や時間が増えることになり、それだけでも品質向上に繋がると感じました。

テスト観点の抽出

実装する担当者ごとにテストの観点が異なっていたら、テスト自体は実装されていたとしても「何がテストされているか」わからず安心してリリースすることは当然できません。
そのため、事前に以下のように必要なテスト観点を整理しました。

POST/PUT 正常系 必須パラメータ全設定
空文字許可パラメータへの空文字設定
必須ではないパラメータの未設定
異常系 必須パラメータの未設定
空文字(or null)禁止パラメータへの空文字(or null)設定
パラメータの型不正
権限不正(他人の情報を更新しようとする等)
GET 正常系 DBの内容を正常に返却(nullの場合もチェック)
異常系 権限不正(他人にメールアドレスなどの情報を公開してしまう等)
DELETE 正常系 DBの内容が意図通り削除される
異常系 権限不正(他人の情報を削除しようとする等)

設計&コーディング

弊社のAPIではLaravelのPassportを使って認証機能を実装しています。したがって、当然のことながらテストでAPIを叩く際にも認証を通過する必要があります。この時、全てのテストクラスで別々に認証作業を実装するのは無駄なので、「認証されたユーザを作成する」といった基本的な機能を提供する親クラス(下記ではPassportTestCase)を作成し、個別のテストはこのクラスを継承する形で実装するよう設計しました。
具体的なコードは以下です。
【PassportTestCase.php】:基本機能を提供する親クラス

class PassportTestCase extends TestCase
{
protected $headersWithToken = [];
protected $user;
use DatabaseTransactions;
public function setUp()
{
parent::setUp();
Artisan::call('migrate');
// PassportのclientIDがなければセットする
$client = DB::table('oauth_personal_access_clients')->first();
if (empty($client)) {
Artisan::call('passport:install');
}
// 認証されたユーザの作成
$this->user = factory(User::class)->create();
$token = $this->user->createToken('TestToken')->accessToken;
// リクエストのヘッダーを設定
$this->headersWithToken['Accept'] = 'application/json';
$this->headersWithToken['Authorization'] = 'Bearer ' . $token;
}
}

【UsersPutTest.php】:具体的なテストクラスの一部(上記の親クラスを継承)

class UsersPutTest extends PassportTestCase
{
const URL_ENDPOINT = '/v1/users';
const HTTP_REQUEST_METHOD = 'PUT';
/**
* パラメータ全設定
*
* @group success
* @group user
*/
public function testSuccessAllSetting()
{
// 更新データ
$newUser = factory(\App\User::class)->make();
$response = $this->withHeaders($this->headersWithToken)
->json(self::HTTP_REQUEST_METHOD, self::URL_ENDPOINT,
[
'name' => $newUser->name,
'introduction' => $newUser->introduction,
'area' => $newUser->area,
'avatar' => UploadedFile::fake()->image('avatar.jpg'),
]);
$response->assertStatus(200);
}
}

まとめ

まだリリース前の開発段階ではありますが、自動テストの効果は絶大です。
弊社では完全なテスト駆動開発ではなく、「機能実装後とりあえずアプリと連携(テスト実装まではwant目標)、リリースまでには必ずテストを実装する」という少し緩い形で運用しているのですが、アプリ側から上がってくるバグ報告のうち、ほとんどはテスト実装が間に合っていなかったものです。じゃあテスト先行の開発にすればいいじゃん、との意見もあると思いますが、このあたりはアプリ開発との兼ね合い(アプリ側でとりあえず早く機能確認したい場合もある)もあるので、これから徐々に全体最適に近づいていければなと思っています。