作りたいものがありすぎる

40歳を過ぎてプログラミングを始めた人の顛末とこれからなど

【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 9章テスト 9-3『WebAPIテスト』

以下の記事は2019年1月10日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読会 (9章 テスト)の輪読会資料の一部となります。 今回は 9-3 章部分の『WebAPIテスト』部分の記事をアップします。

また、元になっている書籍は以下となります。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

  • 9-3 WebAPIテスト
    • 9-3-1 WebAPIテスト機能
    • 9-3-2 テスト対象のAPI
    • 9-3-3 APIテストの実装
    • 9-3-4 WebAPIテストに便利な機能

9-3 WebAPIテスト

WebAPIを検証するテストコードの実装

ここではよりユニットテストより粒度の高い大きいフィーチャーテストの解説となる
LaravelではHTTPリクエストをシミュレートしてテストする機能が用意されている、この機能を利用した例の説明となる。


9-3-1 WebAPIテスト機能

まず下記にあるような単純なAPIを扱う
routes\api.php抜粋

<?php
Route::get('/ping', function () {
    return response()->json(['message' => 'pong']);
});

/api/ping にアクセスすると json [message => 'pong'] が得られるごく単純なAPIとなっている

$ curl  http://larabook.test/api/ping
{"message":"pong"}


テストクラスの生成

テストクラスの生成は artisan make:testコマンドを使う。コマンドにクラス名指定で、 test/Feature/ 以下にテストクラスファイルが生成される。 --unit オプションは使わないので注意

9.3.1.3 フィーチャーテストクラスの生成例

$ php artisan make:test Api/PingTest
Test created successfully.

生成されたファイル tests\Feature\Api\PingTest.phpコマンドでパスを切った場合はディレクトリもよろしく作られる 9.3.1.4 生成されたフィーチャーテストクラスの名前空間

<?php
// 名前空間の生成もフィーチャーテストとしてよろしくやってくれる
namespace Tests\Feature\Api;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

// 省略


HTTPリクエストの送信

HTTPリクエストのテスト実施は、疑似的なHTTPリクエストを送信するメソッドを利用する。 callメソッドはHTTPリクエストを疑似的に送信してくれる。

9.3.1.5 callメソッドの定義

<?php
public function call {
                        // 以下使用の際の引数の順序となっている?
    $method,            // HTTPメソッド
    $uri,               // URI
    $parameters = [],   // 送信パラメータ
    $cookies = [],      // cookie
    $files = [],        // アップロードファイル
    $server = [],       // サーバパラメータ
    $content = null     // RAWリクエストボディ
}

9.3.1.6 callメソッドの実行例

<?php
// GETリクエスト クエリストリング
$response = $this->call('GET', '/api/get?class=motogp&no=99');

// GETリクエスト $parameters
$response = $this->call('GET', '/api/get', [
    'class' => 'motogp',
    'no'    => '99'
]);

// POSTリクエスト
$response = $this->call('POST', '/api/post', [
    'email'    => 'a@example.com',
    'password' => 'secret-password',
]);

また、jsonの場合は以下の定義となる

9.3.1.7 jsonメソッドの定義

<?php
public function json($method, $uri, array $data = [], array $headers = [])

第3引数のリクエストヘッダは自動でも設定される

call,jsonメソッドにはHTTPメソッド毎のラッパー関数が用意されている。 通常はラッパーメソッドを利用し、細かなパラメーター指定が必要な場合にcall,jsonメソッドを利用すると良い。

メソッド 送信する疑似HTTPリクエス
get(\$uri, /$headers = ) GETリクエス
getJson(\$uri, \$headers = ) GETリクエスト(JSON)
post(\$uri, \$data, \$headers = ) POSTリクエス
postJson(\$uri, \$data, \$headers = ) POSTリクエスト(JSON)
put(\$uri, \$data, \$headers = ) PUTリクエス
putJson(\$uri, \$data, \$headers = ) PUTリクエスト(JSON)
patch(\$uri, \$data, \$headers = ) PATCHリクエス
patchJson(\$uri, \$data, \$headers = ) PATCHリクエスト(JSON)
delete(\$uri, \$data, \$headers = ) DELETEリクエス
deleteJson(\$uri, \$data, \$headers = ) DELETEリクエスト(JSON)


HTTPレスポンスのアサーション

送信したHTTPリクエストの結果検証をするアサーションメソッドがある。 Tests\TestCaseクラスにもあるが、 Illuminate\Foundation\Testing\TestResponceクラスの方が特化したアサーションの記述が容易なので作者はこちらを薦めている。

Illuminate\Foundation\Testing\TestResponceクラスの主なアサーションメソッドは以下 9.3.1.10 レスポンスをアサーションする主なメソッド(抜粋)

メソッド 内容
assertStatus(\$status) HTTPステータスコードが引数と一致していれば成功
assertSuccessful() HTTPステータスコードが2XXなら成功
assertRedirect(\$url = null) 次のいずれか 201,301,302,303,307,308 かつ、ヘッダのuriの値が app('uri)->to(\$uri) の値と一致すれば成功
assertHeader(\$headerName, \$value = null) レスポンスヘッダが存在(\$valueがnullの場合)もしくは該当ヘッダの値が$valueと一致すれば成功)
assertExactJson(array \$data, \$strict = false) レスポンスボディのJSONをデコードした配列が$dataと一致すれば成功
assertJson(array \$data, \$strict = false) レスポンスボディのJSONをデコードした配列に$dataが含まれていれば成功

以上を反映した/api/pingAPIテストの例は以下の通り

9.3.1.11 ping APIのテストクラス

<?php
declare(strict_types=1);

namespace Tests\Feature\Api;

use Tests\TestCase;

class PingTest extends TestCase
{
    /**
     * @test
     */
    public function get_ping()
    {
        $response = $this->get('/api/ping');
        // HTTPステータスコードを検証
        $response->assertStatus(200);
        // レスポンスボディのJSONを検証
        $response->assertExactJson(['message' => 'pong']);
    }
}

テストしてみる

$ ./vendor/bin/phpunit tests/Feature/Api/PingTest.php
PHPUnit 6.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 1.26 seconds, Memory: 12.00MB

OK (1 test, 2 assertions)

このようにWebAPIのテストでは疑似的なリクエスト送信をしてレスポンスが検証可能となっている。


9-3-2 テスト対象のAPI

9-2 で行った「データベーステスト」のポイント加算処理を利用して、ポイント加算を行うWebAPIのテストを通じてフィーチャテストの解説。

WebAPIの概要
リクエスJSONに含まれるcustomer_idに該当する顧客に対して add_point で指定したポイントを加算する

9.3.2.2 ポイント加算APIのクラス構成(左下4クラスはDBテストで使用したもの)

f:id:sakamata:20190109035018p:plain

各クラスの概要説明、ソースコードクラス名のリンクをクリックで閲覧可能

AddPointActionクラス app\Http\Actions\AddPointAction.php HTTPに関する処理とユースケースの実行を担う
API処理の親ファイル バリデートを __invoke の引数にフォームリクエストを入れる事で行う。また、 AddPointUseCaseクラスをコンストラクトインジェクションして処理を引っ張ってきてる

AddPointRequestクラス app\Http\Requests\AddPointRequest.php 要はバリデートの為のフォームリクエストファイル customer_idadd_point'required|int' のバリデートをしている

9.3.2.5 ルーティングの追加(routes/api.php)

<?php
// 追加
use App\Http\Actions\AddPointAction;
// 中略
Route::put('/customers/add_point', AddPointAction::class);

AddPointActionクラスはのメソッドは __invoke しているので、クラス名のみの指定となっている

コントローラーやアクションクラスは標準設定だと App\Http\Controller以下に置くことが必要だが、以下の設定を変更することでカスタマイズできる。

9.3.2.6 app\Providers\RouteServiceProvider の設定変更

<?php
// before
// protected $namespace = 'App\Http\Controllers';

// after
protected $namespace = ''; // 空文字にする

AddPointUseCaseクラス app\UseCases\AddPointUseCase.php

配下のAddPointService, EloquentCustomer, EloquentCustomerPointをコンストラクタインジェクションする。
runメソッドで カスタマーの情報(\$customerId, \$pointEvent, \$addPoint, \$now)を引数に、ポイント加算処理を走らせ、返り値として保有したcuntomer_pointtableのpointを返す。

PreConditionException

検証に失敗した際は、app\Exceptions\PreConditionExceptionにtrow そちらに処理は書かれてないが、下記のapp\Exceptions\Handler側でPreConditionExceptionが例外instanceされた際ステータス400とエラーメッセージを出力させる。

app\Exceptions\Handler

9.3.2.9 app\Exceptions\Handlerクラスによるエラーレスポンスの設定(抜粋)

<?php
    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        if ($exception instanceof PreConditionException) {
            return response()->json(['message' => trans($exception->getMessage())], Res::HTTP_BAD_REQUEST);
        }

        return parent::render($request, $exception);
    }

では以上のAPIを実際に使用してみる。 http://larabook.test/api/customers/add_pointにcostomer_id = 1 が 10point Addするcurlコマンドを叩く

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X PUT -d \
'{"customer_id":1,"add_point":10}' http://larabook.test/api/customers/add_point -w "\n"

9.3.2.10 curlコマンドによる実行例

vagrant@homestead:~/lalabook/chapter09$ curl -v -H "Accept: application/json" \
-H "Content-type: application/json" -X PUT -d \
'{"customer_id":1,"add_point":10}' \
 http://larabook.test/api/customers/add_point -w "\n"
*   Trying 192.168.10.10...
* TCP_NODELAY set
* Connected to larabook.test (192.168.10.10) port 80 (#0)
> PUT /api/customers/add_point HTTP/1.1
> Host: larabook.test
> User-Agent: curl/7.58.0
> Accept: application/json
> Content-type: application/json
> Content-Length: 32
>
* upload completely sent off: 32 out of 32 bytes
< HTTP/1.1 200 OK
< Server: nginx/1.14.0 (Ubuntu)
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Cache-Control: no-cache, private
< Date: Tue, 08 Jan 2019 09:00:50 GMT
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
<
* Connection #0 to host larabook.test left intact
{"customer_point":110}

ステータスはHTTP/1.1 200 OKとなる。最後の行にcustomer_pointが110に増加したjsonが返ってきているのが確認できる
当然だが同じコマンドを叩く都度DBが更新され、customer_pointが10増加する

次にエラーとなる値で確認 add_point = 0 でPUTしてみる

vagrant@homestead:~/lalabook/chapter09$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X PUT -d '{"customer_id":1,"add_point":0}' http://larabook.test/api/customers/add_point -w "\n"
*   Trying 192.168.10.10...
* TCP_NODELAY set
* Connected to larabook.test (192.168.10.10) port 80 (#0)
> PUT /api/customers/add_point HTTP/1.1
> Host: larabook.test
> User-Agent: curl/7.58.0
> Accept: application/json
> Content-type: application/json
> Content-Length: 31
>
* upload completely sent off: 31 out of 31 bytes
< HTTP/1.1 400 Bad Request
< Server: nginx/1.14.0 (Ubuntu)
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Cache-Control: no-cache, private
< Date: Tue, 08 Jan 2019 09:15:55 GMT
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
<
* Connection #0 to host larabook.test left intact
{"message":"add_point should be equals or greater than 1"}

ステータスがHTTP/1.1 400 Bad Requestとなって、最後の行にエラーメッセージが返信されてきているのが確認できる。


9-3-3 APIテストの実装

テストでは状況別で以下の4つのケースを実装する

  • ポイント加算が正常完了する
  • バリテーションエラーになる
  • add_pointが事前条件エラーになる
  • customer_idが事前条件エラーになる

以下のクラスにテストを実装してゆく
tests\Feature\Api\AddPointTest

9.3.3.1 tests\Feature\Api\AddPointTestクラス(準備部分を抜粋)

<?php
declare(strict_types=1);

namespace Tests\Feature\Api;

use App\Eloquent\EloquentCustomer;
use App\Eloquent\EloquentCustomerPoint;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AddPointTest extends TestCase
{
    use RefreshDatabase; // <---(1)

    const CUSTOMER_ID = 1;

    protected function setUp()
    {
        parent::setUp();
        //(1-2)
        Carbon::setTestNow();

        // (2) テストに必要なレコードを登録
        factory(EloquentCustomer::class)->create([
            'id' => self::CUSTOMER_ID,
        ]);
        factory(EloquentCustomerPoint::class)->create([
            'customer_id' => self::CUSTOMER_ID,
            'point'       => 100,
        ]);
    }

    // 以下にテストを追加して行く

}

(1) DBを利用しているので use RefreshDatabase; をしている
(1-2)Carbon::setTestNow();テスト用の日時を生成している
PHPで日付時刻の処理を書くなら Carbon がおすすめ より引用

setTestNow() はCarbonが基準とする現在の日時をモックで設定できる機能です。つまりロジック内でCarbonを適切に扱うことで時間に関するテストをユニットテストでも書けるようになります。

(2)テストに必要な値は9.2.2.7同様 factoryを使用して準備する。

正常ケース

APIで100point保持の会員に10point追加した際の動作を確認するシナリオ。
ここまでくると、だいたいパッと見て何やってるか判断できる位にはなりたい所。

9.3.3.1 tests\Feature\Api\AddPointTestクラス(正常ケースのテスト部分を抜粋)

<?php
    /**
     * @test
     */
    // 正常ケースのテスト
    public function put_add_point()
    {
        // (3) API実行
        $response = $this->putJson('/api/customers/add_point', [
            'customer_id' => self::CUSTOMER_ID,
            'add_point'   => 10,
        ]);

        // (4) HTTPレスポンスアサーション
        $response->assertStatus(200);
        $expected = ['customer_point' => 110];
        $response->assertExactJson($expected);

        // (5) データベースアサーション
        $this->assertDatabaseHas('customer_points', [
            'customer_id' => self::CUSTOMER_ID,
            'point'       => 110,
        ]);
        $this->assertDatabaseHas('customer_point_events', [
            'customer_id' => self::CUSTOMER_ID,
            'event'       => 'ADD_POINT',
            'point'       => 10,
            'created_at'  => Carbon::now(),
        ]);
    }


バリテーションエラーとなるケース

9.3.3.2 バリテーションエラーのテストメソッド

<?php
    /**
     * @test
     */
    public function put_add_point_バリデーションエラー()
    {
        // (2) API実行 パラメーター無しでアクセスしてる
        $response = $this->putJson('/api/customers/add_point', [
        ]);

        // (3) HTTPレスポンスアサーション
        $response->assertStatus(422);
        $expected = [
            'message' => 'The given data was invalid.',
            'errors'  => [
                'customer_id' => [
                    'The customer id field is required.',
                ],
                'add_point'   => [
                    'The add point field is required.',
                ],
            ],
        ];
        $response->assertExactJson($expected);
    }

(3) responce 422 はLaravelのバリテーションエラーのステータスコードとなっている。 assertExactJsonメソッドで期待値と一致するかを検証)でエラーメッセージの検証をしているが、レスポンスボティ全体を検証できるメリットと、テストメソッドの可読性を上げることができる為に利用している。

逆にランダムな値を含み、全体一致が難しい値等の検証は。assertJsonメソッド(配列で指定した値が含まれているか検証)を使う

9.3.3.3 assertJsonメソッドでレスポンスJSONの一部を検証

<?php
    /**
     * @test
     */
    public function put_add_point_バリデーションエラー_errorsのみ検証()
    {
        $response = $this->putJson('/api/customers/add_point', [
        ]);

        $response->assertStatus(422);

        // errorsキーのみ検証
        $expected = [
            'errors' => [
                'customer_id' => [
                    'The customer id field is required.',
                ],
                'add_point'   => [
                    'The add point field is required.',
                ],
            ],
        ];
        $response->assertJson($expected);
    }

また、JSONで帰って来た値の中に検証すべきメッセージが存在するかを確認したい場合、レスポンスのJSONを配列に変換して、key別に検証する以下の方法もある
便利な方法なので覚えておくと良い

9.3.3.4 レスポンスボディのJSONを配列に変換して検証

<?php
    /**
     * @test
     */
    public function put_add_point_バリデーションエラー_キーのみ検証()
    {
        $response = $this->putJson('/api/customers/add_point', [
        ]);

        $response->assertStatus(422);

        // レスポンスボディJSONを配列に変換して検証
        $jsonValues = $response->json();

        $this->assertArrayHasKey('errors', $jsonValues);

        $errors = $jsonValues['errors'];
        $this->assertArrayHasKey('customer_id', $errors);
        $this->assertArrayHasKey('add_point', $errors);
    }


add_pointが事前条件エラーとなるケース

add_pointが0かマイナス値の場合正常なエラーメッセージが返るかを検証するテストメソッド
9-1-4 データプロバイダを使っている(配列で検証する値を複数用意して @dataProvider アノテーションで宣言《コメントにメソッド指定》)

復習: @dataProvider アノテーション 一見コメントだが、このメソッドの配列を使ってテストしろって指示出来る奴

<?php
    /**
     * @test
     * @dataProvider dataProvider_put_add_point_add_point事前条件エラー
     */

9.3.3.4 add_pointの事前条件エラーを検証するメソッド

<?php
    /**
     * @test
     * @dataProvider dataProvider_put_add_point_add_point事前条件エラー
     */
    public function put_add_point_add_point事前条件エラー(int $addPoint)
    {
        // (1) API実行
        $response = $this->putJson('/api/customers/add_point', [
            'customer_id' => self::CUSTOMER_ID,
            'add_point'   => $addPoint,
        ]);

        // (2) HTTPレスポンスアサーション
        $response->assertStatus(400);
        $expected = [
            'message' => 'add_point should be equals or greater than 1',
        ];
        $response->assertExactJson($expected);
    }

    public function dataProvider_put_add_point_add_point事前条件エラー()
    {
        return [
            [0],
            [-1],
        ];
    }


customer_idが事前条件とエラーとなるケース

customer_idが存在しないid(999)だった場合に正常なエラーが返るかをテストするメソッド
9.3.3.5 customer_idの事前条件エラーを検証するテストメソッド

<?php
    /**
     * @test
     */
    public function put_add_point_customer_id事前条件エラー()
    {
        // (1) API実行
        $response = $this->putJson('/api/customers/add_point', [
            'customer_id' => 999,
            'add_point'   => 10,
        ]);

        // (2) HTTPレスポンスアサーション
        $response->assertStatus(400);
        $expected = [
            'message' => 'customer_id:999 does not exists',
        ];
        $response->assertExactJson($expected);
    }

最後に該当ファイルのテストを実施してテストが正常に終わる事を確認

vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Feature/Api/AddPointTest.php
PHPUnit 6.5.9 by Sebastian Bergmann and contributors.

........                                                            8 / 8 (100%)

Time: 3.15 seconds, Memory: 20.00MB

OK (8 tests, 20 assertions)

無事正常なテストとして完了した


9-3-4 WebAPIテストに便利な機能

最後にWebAPIテストに便利な機能の紹介
tests\Feature\Api\MiddlewareTest.phpにテストコードが記載されている


ミドルウェアの無効化

HTTPリクエスト送信時にルーティング等で設定されているミドルウェアを無効にできる。
withoutMiddleware メソッドに無効にしたいミドルウェアクラス名を指定すると実行されなくなる

9.3.4.1 ミドルウェアを無効にする

<?php
    /**
     * @test
     */
    public function TeaPotMiddlewareを無効()
    {
        $response = $this->withoutMiddleware(TeaPotMiddleware::class)
            ->getJson('/api/live');

        $response->assertStatus(200);
    }

ミドルウェアを無効にするには withoutMiddlewareを引数無しで実行するか use Illuminate\Foundation\Testing\WithoutMiddleware;を宣言する

9.3.4.2 全ミドルウェアを無効にする

<?php
    /**
     * @test
     */
    public function 全てのミドルウェアを無効()
    {
        $response = $this->withoutMiddleware()
            ->getJson('/api/live');

        $response->assertStatus(200);
    }

9.3.4.3 全ミドルウェアを無効にする(WithoutMiddlewareトレイト)

<?php
declare(strict_types=1);

namespace Tests\Feature\Api;

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class WithoutMiddlewareTest extends TestCase
{
    use WithoutMiddleware;

    /**
     * @test
     */
    public function 全てのミドルウェアを無効()
    {
        $response = $this->getJson('/api/live');

        $response->assertStatus(200);
    }
}


認証

認証が必要なAPIテストには以下の二つの方法がある

tests\Feature\Api\AuthTest.phpにサンプルコードがある

テスト時は通常のリクエストと同様ミドルウェアで認証が実行される

9.3.3.4 認証トークンを送信する例

<?php
    /**
     * @test
     */
    public function guard_api()
    {
        // 認証ユーザーを事前に生成
        factory(User::class)->create([
            'name'      => 'Mike',
            'api_token' => 'token1'
        ]);

        // 認証トークンをリクエストヘッダに設定して送信
        $response = $this->withHeaders([
            'Authorization' => 'Bearer token1'
        ])->getJson('/api/user');

        $response->assertStatus(200);
        $response->assertJson([
            'name' => 'Mike',
        ]);
    }

9.3.4.5 actingAsメソッドによる認証ユーザー設定例

<?php
    /**
     * @test
     */
    public function actingAsで認証ユーザ設定()
    {
        // 認証ユーザーを事前に生成
        $user = factory(User::class)->create([
            'name'      => 'Mike',
            'api_token' => 'token1'
        ]);

        // ミドルウェアを無効にして、actingAsメソッドに認証ユーザを設定
        $response = $this->withoutMiddleware()
            ->actingAs($user)
            ->getJson('/api/user');

        $response->assertStatus(200);
        $response->assertJson([
            'name' => 'Mike',
        ]);
    }

これだとテストを実行するとコントローラーやアクションではAuth::user()等でユーザーを取得できるらしい


コンポーネントのモック(Fake)

MailやNotification等が絡んだ機能でそのままテストをしてしまうと、例えばテストでMailが送信されてしまうが、このようなコンポーネントをモックにすれば、ミドルウェアに接続しないでテストが可能となる。ここではMailのモッククラスを紹介する。

MailのモックはMailファサードのfakeメソッドを利用する。fakeメソッドを利用するとサービスコンテナに登録されているインスタンスがモッククラスに置き換わる

注意 : アサーションメソッドを利用するにはsendメソッドでメールを送信する際に Illuminate\Contracts\Mail\Mailableインターフェースを実装したクラスを引数に指定する必要がある

9.3.4.6 MailFakeのアサーションメソッド

メソッド 内容
assertSent 指定されたメールが送信された事を検証
assertNotSent 指定されたメールが送信されてない事を検証
assertNottingSent メールが送信されてない事を検証
assertQueued 指定されたメールがメールキューに登録された事を検証
assertNotQueued 指定されたメールが送信されてない事を検証
assertNottingQueued メールキューに登録されてない事を検証

9.3.4.7 アサーションメソッドで送信内容が検証できる送信例 (routes\api.php)

<?php
Route::post('/send-email', function (Request $request, Mailer $mailer) {
    // Mailableインターフェースを実装したクラス
    $mail = new \App\Mail\Sample();
    // sendメソッドにMailableインターフェースを実装したクラスを指定
    $mailer->to($request->get('to'))->send($mail);

    return response()->json('ok');
});

9.3.4.8 ファサードのfakeメソッドを利用した例 (tests\Feature\Api\MailTest.php抜粋)

<?php
    /**
     * @test
     */
    public function Mailファサードfakeを利用したテスト()
    {
        Mail::fake(); // <--- (1) MailFakeに置き換え

        $response = $this->postJson('/api/send-email-facade', [
            'to' => 'a@example.com',
        ]);

        $response->assertStatus(200);

        // (2) MailFakeを利用したアサーション
        // 第二引数が数値になると、送信された件数を検証する
        Mail::assertSent(Sample::class, 1);
        // (3) 送信した$mailableの値を検証
        // 第二引数をDIにしてメソッドを検証に使える、hasToは有無の検証なのでbool値が返る
        Mail::assertSent(Sample::class, function (Mailable $mailable) {
            return $mailable->hasTo('a@example.com');
        });
    }

別の方法で、モックを利用せずにメール送信を防ぐ方法もある。logを利用すると送信内容をログファイルに出力する。テスト実行時のみメールドライバを切り替えるには以下の行を追加する。

9.3.4.9 MAIL_DRIVERをlogに設定 (phpunit.xml抜粋)

    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_DATABASE" value="app_test"/>
        <env name="MAIL_DRIVER" value="log"/> <!-- arrayを logにする -->
    </php>

9章は以上となる

まとめ 感想

すげえ疲れた、そもそもテストをするにもそれに至るまで、元のアプリケーション実装を読み込む必要があった、しかし見慣れない書き方満載で、テストに行くまでに調べる事も多く理解するまでが大変だった。しかし理にかなった設計というものを垣間見た気がする。 おそらく筆者の方もテストの章を通じて、アプリケーションの設計思想を見せたかったのやもしれない。
とはいえ、現状なんとかコードを読むことは出来るが、自分で書ける気はまだしない。しかし処理を分離するとテストもし易くなるというメリットはなんとなくつかめた。
最初にフレームワークに触れた時も、当時のベタな書き方に対して、こんなの使いこなせる気がしない!と思ったものだが、やはりしばらくすれば慣れてくるのだろう。

この章の資料を作るにあたってコード以外の部分のスキルも身に付いた。 vagrantの環境追加に始まり開発環境でのsslchromeでも閲覧可能にする方法の確立にはじまったが、エディタにvscodeを使うようになってツールに助けられた部分も大きい。複数のファイル間の呼び出し元クラスへ簡単に渡り歩けてコードを確認できり、ターミナルでシームレスにコマンドを叩いてテスト動作を確認できたりしたのはメリットだった。また、plantUMLを初めて使ってみて、書籍を同じクラス図等が作れるようになったり、自分のアプリケーションのコードと見比べたりも簡単に出来たので、理解の助けになった気がする。ツールはやはり良いものを使うべきだなぁ。という事を実感。 そしてこの3記事に渡るMarkdownのテキストも、全てまとめるとここが今2000行目!かなりのボリュームとなった(ソースコードは書籍GitHubのコピペが大半で申し訳ないが...) 今後は当初の目的であった自分のアプリのテストをゴリゴリ書いてデグレしにくい体制を早く作りたい。