Laravel で API コントローラのテストをする (FeatureTest 編)


Laravel で HTTP コントローラをテストするために FeatureTest を書く。サンプルプロジェクトは [GitHub](https://github.com/grohiro/sandbox-laravel-testing) にあります。

## UnitTest と FeatureTest の違い

Laravel には UnitTest（ユニット・テスト）と FeatureTest（フィーチャー・テスト）の2つのテストがある。
それぞれ保存されるディレクトリが `tests/{Unit,Feature}` と違うだけで、テストの書き方は変わらない。

違いはテストの **対象範囲** だけ。
UnitTest は小さなコードの一部をテストする。単体のモデルやモデルのメソッドを独立してテストする。
FeatureTest は複数のオブジェクトが関連した機能をテストする。主にコントローラのテストに使用する。

## テストを実行する

トップディレクトリで `phpunit` または `./vendor/bin/phpunit` コマンドを実行するとテストが実行される。引数がなければ `tests/` ディレクトリにある全ての UnitTest と FeatureTest が実行される。

```bash
$ phpunit # 全てのテストを実行
$ phpunit tests/Feature/UserControllerTest.php # 特定のテストを実行
```

この他にも `--filter` オプションでテスト対象をフィルタリングできる。詳細は `phpunit` のマニュアル参照。

`phpunit` を実行すると結果が出力される。

```bash
$ 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` を指定して実行する。

```bash
# .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 を初期化することができる。

*テストケースの実行フロー*

全てのテストメソッドに対して、以下のフローで繰り返し実行される。

1. `TeseCase::setUp()` テストケースのセットアップメソッドを実行
1. DB のトランザクションを開始
1. `TestCase::test*()` テストメソッドの実行
  1. 初期データの作成
  2. HTTP リクエスト送信
  3. レスポンス検証
  4. DB検証
1. DB のロールバック

## FeatureTest を作成する

実際にテストケースを作成して `User` モデルを API で操作する `UserController` のテストを書いてみる。

FeatureTest は `php artisan make:test` コマンドで作成できる。

```bash
$ php artisan make:test UserControllerTest
# パスを指定する
$ php artisan make:test Http/Controllers/UserControllerTest
```

作成されたテストクラスに DB 初期化用の `trait` とテスト前に必ず実行される `setUp()` メソッドを追加する。

```php
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` リクエストでデータが返ってくることを検証する。

```php
    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` メソッドを使ったテストも追加する。

```php
    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`に一致するレコードが論理削除されている|


その他のアサーション

- [サンプルプロジェクト](https://github.com/grohiro/sandbox-laravel-testing/blob/master/tests/Feature/UserControllerTest.php)
- [TestCase のアサーション](https://phpunit.readthedocs.io/ja/latest/assertions.html)
- [レスポンスのアサーション](https://laravel.com/docs/5.8/http-tests#available-assertions)
- [DB のアサーション](https://laravel.com/docs/5.8/database-testing#available-assertions)


## Tips
よく利用するイディオムや関連ツール。

### モデルファクトリー
Laravel にはテストデータの作成を簡単にするファクトリーという機能がある。
事前に定義しておいた値で DB にデータを投入できる。

**データの定義**

```php
// 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()` 関数にクラス名を渡す。

```php
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 のマニュアル](https://laravel.com/docs/5.8/database-testing#writing-factories) を参照。

### 認証ユーザ
認証されたユーザを使いたいときは `actingAs()` メソッドでユーザを指定することができる。

```php
// UserControllerTest.php - testIndex()
$user = factory(User::class)->create();
$this->actingAs($user); # 認証済みユーザを指定

// UserController.php - index()
$user = \Auth::user(); // actingAs() で指定したユーザになっている
```

### レスポンスのダンプ
レスポンスの内容を直接確認したいときは `$response->dump()` でダンプすることができる。

### 例外のテスト
例外が発生することをテストしたいときはテストにアノテーションを指定する。

```php
// UserNotFoundeException がスローされること
/**
 * @expectedException \App\Exceptions\UserNotFoudException
 */
public function testIndex()
```

### モック
モックオブジェクトを使いたいときは [Mockery](http://docs.mockery.io/en/latest/) を使用する。

- [MockeryとPHPUnitを使う（最低限） - Qiita](https://qiita.com/zaburo/items/b559782179565bb1c538)

## 参考

- [Testing: Getting Started - Laravel - The PHP Framework For Web Artisans](https://laravel.com/docs/5.8/testing)
- [HTTP Tests - Laravel - The PHP Framework For Web Artisans](https://laravel.com/docs/5.8/http-tests)