LaravelをPHPUnitとPostman でテストする手法まとめ（Passport にも対応）

Laravel で開発されているプロダクトにおいてテストコードを書く方法を、 PHPUnit および Postman を主体にしてまとめます。

# テスト作成の背景

弊社（[株式会社 NoSchool](https://noschool.asia/)）では、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`クラスを継承してください。

```php
final class RoomTest extends TestCase
```

### 各テストケースの基本

各テストケースは、test◯◯ という名称の関数で作成します。

```php
use Tests\TestCase;

final class RoomTest extends TestCase
{
    protected function setUp() // 後述
    {
        parent::setUp();
    }

    public function testSendMessage()
    {
        // hogehoge
    }
```

### API テスト

一つのテストケースの中で、下記のようなフェーズを順に実行します。

-   テストデータの準備
-   API リクエストの実行
-   レスポンスのステータス、Body のテスト
-   必要に応じて、変化したデータベースの中身のテスト

#### テストデータの準備

一般に、各テストケースにおけるテストデータの準備は、`setUp()`メソッドにて実装します。`setUp()`は各テストケースの実行直前に呼ばれます。反対に、終わったあとに呼ばれるのは`tearDown()`です。こちらはデータの後始末などに利用します。

```php
    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`を利用することで当該テストにて作成されたデータは全てテスト終了後に削除されます。

```php
use Illuminate\Foundation\Testing\DatabaseTransactions;

class RoomTest extends TestCase
{
    use DatabaseTransactions;
```

#### API リクエストの実行

API リクエストは下記のように実行します。リクエストするメソッドには複数ありますが、個人的には`json`メソッドが好きです。

```php
$response = $this->json(
    'PUT',
    "/api/chat/{$this->room->id}",
    [
        'body' => 'message
    ],
    $headers
)
```

#### 【補足】Laravel Passport を利用している場合

Passport を使っていると、都度都度 API リクエストに特定のヘッダを指定しなければなりません。具体的には、`Bearer`トークンが必要です。

これを解消するための手段を公開してくれている Web ページが有りましたので、ご参照ください。大まかに言えば、`setUp()`を拡張してそこで`User`に対応したアクセストークンを都度都度発行しています。

https://www.whizz-tech.co.jp/1442/

こうすることで、`json()`実行時の第 4 引数にヘッダーで`Bearer`を渡せば認証済みユーザーとしてテストが実行できます。

また、弊社のネイティブアプリ向けの API の場合、ログアウトユーザーでもクライアント単位でのグラントトークンを Bearer に載せることを必須としているのですが、そのトークンも`setUp()`を使って実装できます。`/oauth/token`へ POST を飛ばしてトークンを都度発行すればいいです。

*補足： https://laravel.com/docs/5.7/passport#testing に書かれている`actingAs`を使った実装は何故か動きませんでした*

#### 【補足】Cookie 認証の API を利用している場合

Cookie 認証の API を利用していて、かつ認証済みのユーザーで API をリクエストしたいときは`actingAs`メソッドを利用します。

```php
$response = $this->actingAs($user)
    ->json('PUT', '/api/room', [
    ]);
```

`factory(User::class)->create()`でテストユーザーを作成後、そのユーザーで認証された状態で API を叩いた場合のテストなどをするときに有効です。

#### レスポンスのステータス、Body のテスト

レスポンスのステータスは、下記のようにテストします。

```php
$response->assertStatus(204);
```

レスポンスの Body は、下記のようにテストします。

```php
$response->assertJson([
    'message_id' => 100
]);
```

この場合のテストは一例で、具体的な値をテストできるものから、JSON の構造のみテストするものまで色々あります。`assertJson`はかなりゆるく、指定したキーを持ってさえいれば、余分なキーが含まれていてもエラーになりません（第 2 引数でより strict にできるようです）。

ここでどうテストするかはテストケースで何をしたいかによるでしょう。ただ、特にネイティブアプリへのレスポンスなど、型にまで気を使わなければいけない場合において、下記のように構造とそれぞれの持っている型を見ることになると思います。

```php
$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 であれば、データベースに変更をもたらすでしょう。

データベースの中身をテストする場合は下記のように記述します。

```php
$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`ヘルパでインスタンスを取得し、そこからメソッドを実行してテストします。

```php
$sendChatMessageUseCase = resolve(SendChatMessageUseCase::class);
```

#### テストしたいクラス内で使っている外部クラスを Mock にしたい場合、DI するのが一番確実っぽい

以下の例はユースケースで使っている Repository をモック化した例です。

```php
$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 なクラスをモックにする意味あるのかって話なのですが。

```php
$requestStub = \Mockery::mock('alias:App\Helpers\HttpClient');
```

といったややこしい仕様が散見されるので、特に`final`なクラスを使うのをやめるか、`\Mockery`を使った単体テストはあまり書かないようにするか、というのが工数的なデメリットを鑑みると妥当かなというのが感想です。より良いライブラリや、Mockery でももっと楽に書けるぞ、という情報提供お待ちしております。

## テストの実行

テストの実行は、`./vendor/bin/phpunit`コマンドで実行します。ディレクトリをオペランドに指定することも、ファイルを指定することも、何も指定せずに全てをテストすることも可能です。

```bash
[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」タブを開き、下記のようにテストを書いていきます。

```javascript
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 つの良い共存方法かと思います。