Laravel で開発されているプロダクトにおいてテストコードを書く方法を、 PHPUnit および Postman を主体にしてまとめます。
テスト作成の背景
弊社(株式会社 NoSchool)では、Web フロントエンドに Nuxt を利用しており、また、iOS ネイティブアプリも開発しているため、Laravel を原則 API ベースで開発することが多いです。
API のテストを作成することは、バグのリスクを低減したり、アプリエンジニアとの仕様の共有のために必須となります。2020 年 1 月現在 NoSchool では PHPUnit
および Postman
を利用していますので、ドキュメントを兼ねてまとめました。
この記事を読むとわかること
- PHPUnit を使って API 単位でのテストを書く方法
- PHPUnit を使って Class 単位での単体テストを書く方法
- Postman を使って API のレスポンスをテストする方法
- それぞれの比較と、実運用する際に共存する方法
環境
- Laravel 5.7(古い...)
- PHPUnit 7.0
- Mockery 1.2
PHPUnit
概要
PHPUnit は、PHP のコードベースでテストを記述できるフレームワークです。
PHPUnit でテストを書く目的
PHP のコードベースのため、データベースのサンプルデータを柔軟に生成し、テスト後に破棄すると言ったことが可能になります。そのため、細かいテストケースを実装するのに向いています。
書き方
ディレクトリ構造
弊社ではtests
ディレクトリ以下に下記のディレクトリ構造でテストを書いていますので、参考までに。
./Feature
結合テスト(API テスト)を書きます。HTTP のエンドポイント単位でのテストを書きます。API を開発したときは原則必須で対応するテストコードを実装しましょう。
./Unit
単体テストを書きます。クラス単位でのテストを書きますが、工数が多くかかること、設計変更に追従するコスト、および Final Class をモック化できない Mockery の制限などの理由から、あまり実装されることはありません。Rendering まで Laravel で行っている一部ページや、クラス単位でのテストをどうしても書きたい場合はこちらに書きます。
./Fixture
テスト用のデータを書きます。アプリ経由の課金で Apple から送信されるレシートなど、細かいデータを表現するときに使います。
./Lib
複数のテストケースの基底となるクラスなどを格納します。
各テストコードの基本
各テストコードは、それぞれのディレクトリ以下にドメインごとに namespace を切って作成してください。また、命名は ◯◯Test.php で作成してください。
ex. ./Feature/Chat/RoomTest.php
また、各テストケースはTests\TestCase
クラスを継承してください。
final class RoomTest extends TestCase
各テストケースの基本
各テストケースは、test◯◯ という名称の関数で作成します。
use Tests\TestCase;
final class RoomTest extends TestCase
{
protected function setUp() // 後述
{
parent::setUp();
}
public function testSendMessage()
{
// hogehoge
}
API テスト
一つのテストケースの中で、下記のようなフェーズを順に実行します。
- テストデータの準備
- API リクエストの実行
- レスポンスのステータス、Body のテスト
- 必要に応じて、変化したデータベースの中身のテスト
テストデータの準備
一般に、各テストケースにおけるテストデータの準備は、setUp()
メソッドにて実装します。setUp()
は各テストケースの実行直前に呼ばれます。反対に、終わったあとに呼ばれるのはtearDown()
です。こちらはデータの後始末などに利用します。
protected function setUp()
{
parent::setUp();
$this->room = factory(Room::class)->create([
'user_id' => 1111
]);
}
ここでのポイントは大きく 2 点あります。
まず、parent::setUp()
を実行することです。これを実行することで、Laravel アプリケーションを動かすための初期設定が終わります。実行しない場合、例えばconfig()
を使った実装等が動かないので必ず実行してください。
詳しい実装は/Illuminate/Foundation/Testing/TestCase.php
に書いてあります。
2 つめのポイントはfactory()
グローバルヘルパを利用してテストデータを生成することです。
ファクトリはdatabase/factories
以下に作成します。詳しい書き方は実際のコードを読んだり、公式ドキュメントを参照してください。ここで重要なのは以下の 2 点です。
- factory を create するタイミングでテストデータを Array で渡すことで上書きできる
- factory 自体に名前をつけることができるため、例えば同じ User Model に対する factory でも違う初期データを持ったインスタンスを作り分けることができる
ファクトリ生成後は、実データがデータベースにインサートされているため、それがある前提で以降のテストを書くことができます。
【補足】テストデータの削除
テストデータを毎回テストのたびに生成していると、データベースがテストデータで溢れかえってしまいます。ここで、DatabaseTransactions
を利用することで当該テストにて作成されたデータは全てテスト終了後に削除されます。
use Illuminate\Foundation\Testing\DatabaseTransactions;
class RoomTest extends TestCase
{
use DatabaseTransactions;
API リクエストの実行
API リクエストは下記のように実行します。リクエストするメソッドには複数ありますが、個人的にはjson
メソッドが好きです。
$response = $this->json(
'PUT',
"/api/chat/{$this->room->id}",
[
'body' => 'message
],
$headers
)
【補足】Laravel Passport を利用している場合
Passport を使っていると、都度都度 API リクエストに特定のヘッダを指定しなければなりません。具体的には、Bearer
トークンが必要です。
これを解消するための手段を公開してくれている Web ページが有りましたので、ご参照ください。大まかに言えば、setUp()
を拡張してそこでUser
に対応したアクセストークンを都度都度発行しています。
こうすることで、json()
実行時の第 4 引数にヘッダーでBearer
を渡せば認証済みユーザーとしてテストが実行できます。
また、弊社のネイティブアプリ向けの API の場合、ログアウトユーザーでもクライアント単位でのグラントトークンを Bearer に載せることを必須としているのですが、そのトークンもsetUp()
を使って実装できます。/oauth/token
へ POST を飛ばしてトークンを都度発行すればいいです。
補足: https://laravel.com/docs/5.7/passport#testing に書かれているactingAs
を使った実装は何故か動きませんでした
【補足】Cookie 認証の API を利用している場合
Cookie 認証の API を利用していて、かつ認証済みのユーザーで API をリクエストしたいときはactingAs
メソッドを利用します。
$response = $this->actingAs($user)
->json('PUT', '/api/room', [
]);
factory(User::class)->create()
でテストユーザーを作成後、そのユーザーで認証された状態で API を叩いた場合のテストなどをするときに有効です。
レスポンスのステータス、Body のテスト
レスポンスのステータスは、下記のようにテストします。
$response->assertStatus(204);
レスポンスの Body は、下記のようにテストします。
$response->assertJson([
'message_id' => 100
]);
この場合のテストは一例で、具体的な値をテストできるものから、JSON の構造のみテストするものまで色々あります。assertJson
はかなりゆるく、指定したキーを持ってさえいれば、余分なキーが含まれていてもエラーになりません(第 2 引数でより strict にできるようです)。
ここでどうテストするかはテストケースで何をしたいかによるでしょう。ただ、特にネイティブアプリへのレスポンスなど、型にまで気を使わなければいけない場合において、下記のように構造とそれぞれの持っている型を見ることになると思います。
$response->assertJsonStructure([
'id',
'user' => [
'id',
'name',
],
'created_at',
]);
$this->assertIsInt($response->json('id'));
$this->assertIsInt($response->json('created_at'));
$this->assertIsInt($response->json('user.id'));
$this->assertIsString($response->json('user.name'));
ネストしているキーに、Laravel にあるあるのドット記法でアクセスできるのは便利ですね。
詳しくは公式情報をどうぞ。
https://laravel.com/api/5.7/Illuminate/Foundation/Testing/TestResponse.html
https://phpunit.readthedocs.io/ja/latest/assertions.html#assertisint
必要に応じて、変化したデータベースの中身のテスト
副作用のある API であれば、データベースに変更をもたらすでしょう。
データベースの中身をテストする場合は下記のように記述します。
$this->assertEquals(
3,
Room::where('body', 'hogehoge')->count()
);
https://readouble.com/laravel/5.7/ja/database-testing.html を見る限り、assertDatabaseHas
というメソッドも用意されています。しかし、この場合はカウントで 2 つ以上のものがあること、というのは確認できませんので、用途に応じて使い分けてください。
このように Eloquent Model を使ってクエリを発行して結果をチェックというのは少々我ながら筋が悪いため、より上手に書ける方法を知っている方は教えていただけると嬉しいです。
単体テスト
単体テストは、前述したとおり結構モック化を頑張らないと厳密な単体テストの実現が難しいです。
もし実装したい場合は、下記のようなコツを念頭に実装してみてください。
ここではユースケース層のテストを例に話します。NoSchool の場合、ユースケース層からは Repository の Interface を呼んでおり、AppServiceProvider
で実装クラスにbinding
しているという特徴を持っているため、その前提での解説となります。
DI を使っている場合はresolve
ヘルパでインスタンスを取得
DI している場合は普通にコンストラクタを書けないはずなので、resolve
ヘルパでインスタンスを取得し、そこからメソッドを実行してテストします。
$sendChatMessageUseCase = resolve(SendChatMessageUseCase::class);
テストしたいクラス内で使っている外部クラスを Mock にしたい場合、DI するのが一番確実っぽい
以下の例はユースケースで使っている Repository をモック化した例です。
$repository = Mockery::mock(new MemberRepository($this->user));
$repository->shouldReceive('findMember')->once()->andReturn(
$member
);
$repository->shouldReceive('createMember')->once()->andReturn($member);
$this->app->instance(
'App\Infrastructure\MemberRepository',
$repository
);
final クラスをモック化するときはalias:
記法を使う
final なクラスをモックにするときはalias:
を先頭につけて namespace を記述します。なんじゃこりゃ。まあ、そもそも final なクラスをモックにする意味あるのかって話なのですが。
$requestStub = \Mockery::mock('alias:App\Helpers\HttpClient');
といったややこしい仕様が散見されるので、特にfinal
なクラスを使うのをやめるか、\Mockery
を使った単体テストはあまり書かないようにするか、というのが工数的なデメリットを鑑みると妥当かなというのが感想です。より良いライブラリや、Mockery でももっと楽に書けるぞ、という情報提供お待ちしております。
テストの実行
テストの実行は、./vendor/bin/phpunit
コマンドで実行します。ディレクトリをオペランドに指定することも、ファイルを指定することも、何も指定せずに全てをテストすることも可能です。
[mejileben]$ ./vendor/bin/phpunit ./tests/Feature/Room
上記のコマンドでは、Room
ディレクトリ以下のテストケースを全て実行します。
コマンドを覚えたくない方は、composer
やartisan
コマンドとしてエイリアスを貼ってもいいでしょう。
Postman
概要
Postman は、API のドキュメントを書いたり、テストケースを書いてまとめて複数の API をテストできるサービスです。
Swagger のテスト実行機能がついたようなイメージです。Swagger を使ったことがないのでイメージですが。
Postman を導入する目的
Postman を導入することで、API ドキュメントをネイティブアプリエンジニアに Web 経由で共有することができます。NoSchool では API 定義を JSON でエクスポートし、Git 管理しています。これを各自が Import することで API 定義を確認できます。有料プランに加入することで複数人が Web 上で共有を完結できるそうですが、ケチっているのでまだ無料プランです。
Postman 環境設定の概要
詳しくは割愛しますが、主に以下の点に注意が必要です。
- 秘匿情報は Environment として管理し、GitHub には上げない。または暗号化する
- ローカル環境で HTTPS を利用しているとき、設定画面から SSL のチェックを無効化しないとオレオレ証明書でエラーを吐いてしまう
Postman を使った API テストのしかた
Postman で API 定義を作成し、試しに「Send」ボタンを押すと、レスポンスが返ってきます。
ここまで終わったら、「Tests」タブを開き、下記のようにテストを書いていきます。
var jsonData = pm.response.json();
pm.test("id is number", function() {
pm.expect(jsonData.id).to.be.a("number");
});
pm.test("room.id is 1540", function() {
pm.expect(jsonData.room.id).to.eq(1540);
});
Postman のテストケースはJavaScript
のライブラリであるChai
を利用しているため、JavaScript ベースで書くことができます。
まずjson()
メソッドでレスポンスの JSON を取得します。
次にpm.test()
でテストケースを作成します。expect()
でテストしたい値を JSON から抜き取り、その後は英文を書くかのようにメソッドチェーンで.to.be.a
などとつないでいき、最後に型名か具体的な値を書くことでテストが実行されます。
ここでは.to
や.be
をつけることは必須ではないのですが、そのほうが英文っぽく書けるので好ましいと思っています。それだけです。Rspec 等と思想が似ていると思います。
Postman のテストでは具体的な値等までテストすることは難しいので、基本的にレスポンスの型のみテストすることが大半です。
Collection を作る
さて、API テストをいくつか作成すると、Collection というものにまとめることができます。Collection は、例えば Q&A サイトの場合、「質問したあとに回答がつき、それをベストアンサーにする」といった複数の連続した API のテストをすることが可能です。
テストを実行する
テストの実行は、クライアントアプリケーション上からもできるし、CLI から Node.js 等のコマンドベースでも実行可能です。
Postman を利用するメリットは?
API テストの表現力という意味では PHPUnit で頑張るほうが身の丈に合ってそうですが、例えば検証環境など、HTTP 越しでアクセスしたい場合は Postman でリクエストの向き先を Localhost から検証環境に向けて実行することができるなど、手軽さという意味では Postman に軍配があがるイメージです。
ただ、CI を組み込んだ場合などは結局 PHPUnit を検証環境で実行することなど簡単でしょうから、Postman でテストを頑張るメリットは薄れ、Swagger 等と同様で API の定義を複数の社員間で共有することがメイン目的となるでしょう。
個人的には Passport を使っている環境でも PHPUnit ベースでテストできることが衝撃でした。先の記事を公開した方に感謝です。
まとめ
PHPUnit も Postman も両方 API ベースでのテストの記述が可能ですが、PHPUnit のほうがより細かい制御が可能です。Postman は API 定義をまとめるのに特化して、PHPUnit でできる限りテストを書いていくのが 1 つの良い共存方法かと思います。