Laravel で HTTP コントローラをテストするために FeatureTest を書く。サンプルプロジェクトは GitHub にあります。
UnitTest と FeatureTest の違い
Laravel には UnitTest(ユニット・テスト)と FeatureTest(フィーチャー・テスト)の2つのテストがある。
それぞれ保存されるディレクトリが tests/{Unit,Feature} と違うだけで、テストの書き方は変わらない。
違いはテストの 対象範囲 だけ。
UnitTest は小さなコードの一部をテストする。単体のモデルやモデルのメソッドを独立してテストする。
FeatureTest は複数のオブジェクトが関連した機能をテストする。主にコントローラのテストに使用する。
テストを実行する
トップディレクトリで phpunit または ./vendor/bin/phpunit コマンドを実行するとテストが実行される。引数がなければ tests/ ディレクトリにある全ての UnitTest と FeatureTest が実行される。
$ phpunit # 全てのテストを実行
$ phpunit tests/Feature/UserControllerTest.php # 特定のテストを実行
この他にも --filter オプションでテスト対象をフィルタリングできる。詳細は phpunit のマニュアル参照。
phpunit を実行すると結果が出力される。
$ phpunit
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 118 ms, Memory: 16.00 MB
OK (2 tests, 2 assertions)
最終行の OK (2 tests, 2 assertions) は2つのテストが実行されて 2つの検証(アサーション)にパスしたことが表示されている。
テストの設定
phpunit を実行すると Laravel の App::environment() は testing に設定される。.env を読み込んだ後に .env.testing で上書きされるので、変更したい値のみ設定することができる。
テストに使用する DB のマイグレーションは --env testing を指定して実行する。
# .env.testing で指定されたDBのマイグレーション
$ php artisan migrate --env testing
# DBのリセット
$ php artisan migrate:fresh --env testing
PHPUnit の設定は phpunit.xml で変更できる。
テストの全体像
全てのテストクラスは PHPUnit の TestCase クラスを継承する。継承したクラスは「テストケース」と呼ばれ、複数の「テスト」メソッドを保持する。
phpunit コマンドが実行されると、クラス名が Test で終わっているクラスを読み込み、全ての test で始まるメソッドを実行する。
DB はテスト毎にデータを初期化して空の状態で開始される。
Laravel には DB のトランザクションをロールバックする trait DatabaseTransactions があり、これを利用することでテスト実行後に DB を初期化することができる。
テストケースの実行フロー
全てのテストメソッドに対して、以下のフローで繰り返し実行される。
-
TeseCase::setUp()テストケースのセットアップメソッドを実行 - DB のトランザクションを開始
-
TestCase::test*()テストメソッドの実行 - 初期データの作成
- HTTP リクエスト送信
- レスポンス検証
- DB検証
- DB のロールバック
FeatureTest を作成する
実際にテストケースを作成して User モデルを API で操作する UserController のテストを書いてみる。
FeatureTest は php artisan make:test コマンドで作成できる。
$ php artisan make:test UserControllerTest
# パスを指定する
$ php artisan make:test Http/Controllers/UserControllerTest
作成されたテストクラスに DB 初期化用の trait とテスト前に必ず実行される setUp() メソッドを追加する。
use Illuminate\Foundation\Testing\DatabaseTransactions;
class UserControllerTest extends TestCase
{
// データベースの初期化にトランザクションを使う
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
}
public function testIndex()
{
// 検証(assert) をココに書く
}
}
実際のプロジェクトではベースとなる TestCase クラスを作成して共通化する
index アクションのテスト
index アクションの仕様は GET リクエストを送信すると users テーブルのデータを JSON の配列で返す API を想定する。
テストでは事前に1件のデータを作成して GET リクエストでデータが返ってくることを検証する。
public function testIndex()
{
// `users` テーブルにデータを作成 (Tips参照)
factory(User::class)->create([
'email' => 'user1@example.com',
]);
// GET リクエスト
$response = $this->get(route('users.index'));
// レスポンスの検証
$response->assertOk() # ステータスコードが 200
->assertJsonCount(1) # レスポンスの配列の件数が1件
->assertJsonFragment([ # レスポンスJSON に以下の値を含む
'email' => 'user1@example.com',
]);
}
$this->get() を呼び出すと GET リクエストを送信することができる。レスポンス $response を取得したら検証用のメソッド assert* を呼び出して正しいレスポンスが返ってきていることを検証している。
このテストを書いて phpunit を実行すると以下のように表示される。
$ phpunit tests/Feature/UserControllerTest.php
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 130 ms, Memory: 18.00 MB
OK (1 test, 3 assertions)
1つのテストが実行されて3件のアサーションをパスした。
POST DELETE メソッドを使ったテストも追加する。
public function testStore()
{
$data = [ # 登録用のデータ
'user' => [
'email' => 'user@example.com',
'name' => 'Tanaka Taro',
'password' => '123456',
],
];
// POST リクエスト
$response = $this->post(route('users.store'), $data);
// レスポンス検証
$response->assertOk() # ステータスコードが 200
->assertJsonFragment([ # レスポンス JSON に以下の値を含む
'email' => 'user@example.com',
'name' => 'Tanaka Taro',
]);
}
public function testDestroy()
{
// `users` テーブルにデータを作成
$user = factory(User::class)->create([
'email' => 'user1@example.com',
]);
// DELETE リクエスト
$response = $this->delete(route('users.destroy', [$user->id]));
// ステータスコード 200
$response->assertOk();
// `users` テーブルが0件になっている
$this->assertEquals(0, User::count());
}
phpunit を再実行する。
$ phpunit tests/Feature/UserControllerTest.php
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 174 ms, Memory: 20.00 MB
OK (3 tests, 8 assertions)
3件のテストが実行され、8件のアサーションをパスした。
検証 (アサーション)
1つのテストに複数のアサーションを書くことができる。アサーションが1つでも失敗すると、そのテストは失敗(F)として表示される。
レスポンスクラスの $response->assert* メソッドはレスポンスを対象にした検証メソッドが実装されている。
TeseCase クラスの $this->assert* メソッドは値の一致、不一致、null チェックなどレスポンスに依存しないメソッドが実装されている。
また、上記の他に DB の内容を直接検証できるメソッドも存在する。
よく利用する検証メソッド
| メソッド名 | クラス | 説明 |
|---|---|---|
assertEquals($a, $b) |
TestCase |
$aと$bが一致する |
assertTrue($b) |
TestCase |
$bがtrue
|
assertNotNull($o) |
TestCase |
$oがnullではない |
assertArraySubset($subset, $a) |
TestCase |
配列$aに部分配列$subsetを含む |
assertOk() |
レスポンス | ステータスコードが200
|
assertJsonCount($n) |
レスポンス(JSON) | JSON配列レスポンスの件数が$n件 |
assertHeader($h, $v) |
レスポンス | レスポンスヘッダ$hの値が$nである |
assertSee($t) |
レスポンス(HTML) | HTMLに文字列$tが存在する |
assertDatabaseHas($t, array $a) |
TestCase |
テーブル$tに$aに一致するレコードが存在する |
assertSoftDeleted($, array $a) |
TestCase |
テーブル$tの$aに一致するレコードが論理削除されている |
その他のアサーション
Tips
よく利用するイディオムや関連ツール。
モデルファクトリー
Laravel にはテストデータの作成を簡単にするファクトリーという機能がある。
事前に定義しておいた値で DB にデータを投入できる。
データの定義
// database/factories/UserFactory.php
// User モデルのファクトリクラス
use Illuminate\Support\Str;
use Faker\Generator as Faker;
use App\User;
// define() で事前データを定義する
$factory->define(App\User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'email_verified_at' => now(),
'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
'remember_token' => Str::random(10),
];
});
// データに名前を付けて複数定義することもできる
$factory->state('user2')->define(...);
データを作成するときは factory() 関数にクラス名を渡す。
use App\User;
// DBに保存された User モデルが返る
$user = factory(User::class)->create();
// 特定の値だけ上書きして作成する
$user = factory(User::class)->create([
'id' => 1,
'email' => 'tanaka@example.com',
]);
// 10件作成
$user = factory(User::class, 10)->create();
// DBに保存せず new User() したモデルを取得
$user = factory(User::class)->make();
// 名前を付けたモデルを取得
$user = factory(User::class)->states('user2')->make();
詳細は Laravel のマニュアル を参照。
認証ユーザ
認証されたユーザを使いたいときは actingAs() メソッドでユーザを指定することができる。
// UserControllerTest.php - testIndex()
$user = factory(User::class)->create();
$this->actingAs($user); # 認証済みユーザを指定
// UserController.php - index()
$user = \Auth::user(); // actingAs() で指定したユーザになっている
レスポンスのダンプ
レスポンスの内容を直接確認したいときは $response->dump() でダンプすることができる。
例外のテスト
例外が発生することをテストしたいときはテストにアノテーションを指定する。
// UserNotFoundeException がスローされること
/**
* @expectedException \App\Exceptions\UserNotFoudException
*/
public function testIndex()
モック
モックオブジェクトを使いたいときは Mockery を使用する。