勉強日記

チラ裏

PHPフレームワーク Laravel Webアプリケーション開発 第11章 輪読会資料

www.socym.co.jp

この本の輪読会

冗談みたいに長いんですけど

テスト駆動開発(TDD: Test-Driven Development)とは

  • Kent Beck氏が考案した手法
    • XP: Extreme Programming の考案者
  • 「きれいな実装」で「きちんと動作する」ソースコードを目指す
  • いきなり「きれいな実装」を目指さない
    1. きちんと動作することを確認するためのテストを書く
    2. 実装前に、テストが失敗することを確認する
      • 【補】ちゃんとテストが実施されていることを確認
    3. きちんと動作する実装をできるだけ素早く行う
      • 汚くていい
    4. テストが成功することを確認する
    5. テストが失敗しないことを確認しながら、きれいな実装を目指してリファクタリングする

コツはできるだけ小さく

  • 最重要 by 執筆陣
  • すべてにつけてそう
    • テスト作成時
    • 最初の実装にかける時間
    • リファクタ中のテスト実行間隔
  • 効能
    • 集中力が途切れにくくなる
    • 開発作業にリズムが生まれる

本章のねらい

  • 以下を体験する
    • 安心感
      • 必要な機能が満たされていることが、テストにより保証される
    • 高揚感
      • 短いサイクルで集中してリズムよく開発
    • 達成感
      • コードが徐々にきれいになっていく

サンプルアプリケーション仕様

  • 略(pp.467-468)
  • モバイルアプリケーションに利用されるAPI
    • モバイルアプリケーションそのものは作らない

データベース仕様

  • 略(pp.468-470)

APIエンドポイント

  • 略(pp.470-471)

APIエンドポイントの作成

アプリケーションの作成・事前準備

環境

プロジェクト作成

composer config -g repos.packagist composer https://packagist.jp # 近くのリポジトリに
composer global require hirak/prestissimo # 高速化プラグイン

composer create-project --prefer-dist laravel/laravel tdd_sample "5.5.*"

【補】単一Homestead上で複数Laravelプロジェクトを動かし、http://homestead.tdd-sample/でアクセスできるようにする

/path/to/Homestead/Homestead.yaml

---
ip: "192.168.10.10"
memory: 2048
cpus: 1
provider: virtualbox

authorize: ~/.ssh/id_rsa.pub

keys:
    - ~/.ssh/id_rsa

folders:
    - map: ~/code
      to: /home/vagrant/code
      

sites:
    - map: homestead.test
      to: /home/vagrant/code/sampleapp/public
+   - map: homestead.tdd-sample
+     to: /home/vagrant/code/tdd_sample/public

databases:
    - homestead

ホストマシン側のhosts追記(適宜sudo chmodして)

 ##
 # Host Database
 #
 # localhost is used to configure the loopback interface
 # when the system is booting.  Do not change this entry.
 ##
 127.0.0.1  localhost
 255.255.255.255    broadcasthost
 ::1             localhost 
 192.168.10.10  homestead.test
+192.168.10.10  homestead.tdd-sample

設定再読み込み

vagrant reload

要らんもの消す

rm -rf tests/Feature/ExampleTest.php \
       tests/Unit/ExampleTest.php \
       database/migrations/2014_10_12_000000_create_users_table.php \
       database/migrations/2014_10_12_100000_create_password_resets_table.php 

最初のテスト

TODOリストを作成する

  • まず
    • APIエンドポイントに
    • 各HTTPメソッドで
    • アクセスできること
  • TODOリスト
    • api/customersGETメソッドでアクセスできる
    • api/customersPOSTメソッドでアクセスできる
    • ...(略)
  • 最初から完璧じゃなくていい

テストファイルの作成

  • エンドポイントへのアクセス確認 = 機能テストを作成
php artisan make:test ReportTest
  • ファイル生成確認
tree tests
tests/
├── CreatesApplication.php
├── Feature
│   └── ReportTest.php
├── TestCase.php
└── Unit

テストメソッドを追加

  • TODOの項目をそのままテストメソッド名に
    • テストが何を検証しているか一目瞭然

テストメソッドに何をどのように書くか

最初に「検証」部分から記述

<?php
    /**
     * @test
     */
    public function api_customersにGETメソッドでアクセスできる()
    {
        $response->assertStatus(200);
    }
./vendor/bin/phpunit
ErrorException: Undefined variable: response
  • assertから書けってこと
    • 変数未定義とかでエラーが出てOK

次に「検証」する結果を取得する「実行」を記述

    public function api_customersにGETメソッドでアクセスできる()
    {
+       $response = $this->get('api/customers');
        $response->assertStatus(200);
    }

bashでは、直前のコマンドを!!で呼べますね

!!
1) Tests\Feature\ReportTest::api_customersにGETメソッドでアクセスできる
Expected status code 200 but received 404.
Failed asserting that false is true.
  • やっとこさテストが動くようになる(通るとは言ってない)

最低限の実装

  • とりあえずテストが通るように

/routes/api.php

    Route::middleware('auth:api')->get('/user', function (Request $request) {
        return $request->user();
    });

+   Route::get('customers', function () {});

bash

!!
OK (1 test, 1 assertion)

2つ目以降のテスト

  • 略(pp.478-480)

1つのテストメソッドに検証は1つの原則

1) Tests\Feature\ReportTest::すべてのエンドポイントへアクセスできる
Expected status code 200 but received 404.
Failed asserting that false is true.

どのassert!?

テストコードの確認

  • 略(pp.481-484)

テストに備えるデータベース設定

  • TDDの肝は、「何度も繰り返しテストを実行すること」
  • DBテストも冪等でなければならない

データベース設定

テスト用DBつくる

bash

mysql

mysql

create database test_database;
Query OK, 1 row affected (0.01 sec)
show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| homestead          |
| mysql              |
| performance_schema |
| sys                |
| test_database      |
+--------------------+
6 rows in set (0.01 sec)

【補】権限付与

  • laradockデフォルトは以下
    • user: default
    • database: default
    • password: secret
  • defaultユーザーではdatabaseを作れないので、rootで入る
  • CREATE DATABASE test_database;する
  • defaultユーザーが権限を持っていないので、テストで使用する句をGRANTする
    • grant create,update,insert,alter,drop,delete on *,* to 'default'@'%'
    • ちゃんとメモしてないのでなんか足りないかも
      • テスト時にエラーが出るので適宜GRANTする

テスト用DB設定する

次節(11-4)でよくない???

.env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead  # これをphpunit実行時だけ挿げ替えたい
DB_USERNAME=homestead
DB_PASSWORD=secret

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="test_database"/>
    </php>

マイグレーション・モデル・ファクトリ

マイグレーション・モデル・ファクトリつくる

php artisan make:modelの仕様

php artisan help make:model
Usage:
  make:model [options] [--] <name>

Arguments:
  name                  The name of the class

Options:
  -a, --all             Generate a migration, factory, and resource controller for the model
  -c, --controller      Create a new controller for the model
  -f, --factory         Create a new factory for the model
      --force           Create the class even if the model already exists.
  -m, --migration       Create a new migration file for the model.
  -p, --pivot           Indicates if the generated model should be a custom intermediate table model.
  -r, --resource        Indicates if the generated controller should be a resource controller.
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

php artisan make:model Customer -mf  # -m: マイグレーションも作る
php artisan make:model Report   -mf  # -f: ファクトリも作る

マイグレーションの編集

  • 略(pp.487-489)
  • BlueprintのAPI
    • integerのオプショナル引数
      • 第二: 自動増加するかどうか(デフォルトfalse = 自動増加しない)
      • 第三: 符号なしかどうか(デフォルトfalse = 符号つき)
-           $table->integer('customer_id', false, true);
+           $table->integer('customer_id')->unsigned();

モデルにIDE HelperでphpDocsを付与

  • 略(pp.490-491)

Factoryの編集

  • 略(p.492)
  • FK制約がある場合は好き勝手な値を入れられないので、factory呼び出し側で設定する

初期データ投入用シーダーの準備

どうでもいいが、DBにデータを投入することを、英語でpopulateと言うそうですね

シーダーつくる

  • 略(p.493)

シーダーいじる

<?php
    public function run()
    {
        factory(\App\Customer::class, 2)
            // Customerは生成と同時に保存
            ->create()
            ->each(
                function ($customer) {
                    factory(\App\Report::class, 2)
                        // FK `reports.customer_id` は
                        // PK`customers.id`に紐づける必要があるため、
                        // まだ保存しない                        
                        ->make()
                        ->each(
                            function ($report) use ($customer) {
                                //  customerに紐づけて保存
                                $customer->reports()->save($report);
                            });
                });
    }
  • makeとcreateのちがい
    • make
      • Eloquentのsave()を呼び出す必要あり
    • create
      • Eloquentのsave()呼ばれる

マイグレーション・シーディング

php artisan migrate
php artisan db:seed --class=TestDataSeeder

mysql

use homestead;  -- まだテスト用DBは使ってない

select * from customers;
+----+---------------------+---------------------+---------------------+
| id | name                | created_at          | updated_at          |
+----+---------------------+---------------------+---------------------+
|  1 | 有限会社 木村       | 2019-01-14 10:12:31 | 2019-01-14 10:12:31 |
|  2 | 有限会社 笹田       | 2019-01-14 10:12:31 | 2019-01-14 10:12:31 |
+----+---------------------+---------------------+---------------------+

select * from reports;
+----+------------+-------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+
| id | visit_date | customer_id | detail                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | created_at          | updated_at          |
+----+------------+-------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+
|  1 | 1996-06-09 |           1 | ロスへ着ついた小さな鳥どりの男の子をジョバンニは、けれどもなくなって、ぼくいががらん。わたくわらの礫こい鋼はがねえさんはもうして、とがったようにあたりはこをこすっかりの景気けいきなぼたんでいるようと船の沈しずかに棲すんでかくれていました。「どこまですよ。ずいてあるようになって」「するように立ち直なおぼつか蠍さそりは、また、高く高く口笛くちを通ってらあのね、ほうさな鼠ねずみます。そこにいました。み。                                                                                                                                                                                                         | 2019-01-14 10:12:31 | 2019-01-14 10:12:31 |
|  2 | 1988-01-25 |           1 | なっておや、またそうと言いっぱな機関車きからここはケンタウル祭さい。けれどもが、青い森の上着うわぎの第二時だい」鳥捕とりが川へは帰らず、急いそい鉄てつどうして見たままになった」そのまって、きちんといわれ、そこらは、どこまでも堅かたちやなんでにどん電燈でんとうを持もっていたよ。もうそこへ行って言いいえずかにその火が燃もえるじゃくやなんと光らせなかに流ながら言いい、ザネリはどうでしょで遠くだと言いま。                                                                                                                                                                                                         | 2019-01-14 10:12:31 | 2019-01-14 10:12:31 |
|  3 | 2015-12-21 |           2 | こいでしょう」ジョバンニは、「今晩こんやり白い岩いわねえ」ジョバンニは」「あの水が深いほかの神かみさまざまの前の方を見るほどありました。線路せんの格子こうの」ジョバンニさん、今夜ケンタウル祭さい」あの銀河ぎんやりしていましたようなものをひきましょうかこともまた泪なみの間から、少しわらか、まるでもいなんかくに見えずに博士はかすか」そした。「ああわあとの丘おかによりもうそのうちに向むこうのほのお祭ま。                                                                                                                                                                                                         | 2019-01-14 10:12:31 | 2019-01-14 10:12:31 |
|  4 | 1989-05-11 |           2 | まるでもわかにカムパネルラがいつかまた、あら、手をあつました。その街燈がいきをした。あんな雁がんがを大きさせて睡ねむってるんでいる間そのひびきや風につらい)ジョバンニは、ここどもらっしょうど十二ばかりにすわっしゃしんぱんの豆電燈でんといっして、ジョバンニを見ました。インデアンの塊かたちしっかり汽車を追おっかさな水夫すいや黄いろなんかくひょうへ出ているのです。「まあおととも言いっぱに光っていたいあ。                                                                                                                                                                                                         | 2019-01-14 10:12:31 | 2019-01-14 10:12:31 |
+----+------------+-------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+

データベーステスト

テスト用トレイトの利用・初期データの投入

<?php
class ReportTest extends TestCase
{
    // 各テストメソッドについて
    // - 実行前のマイグレーション
    // - 実行後のロールバック
    // を自動的にやってくれるトレイト
    use RefreshDatabase;

    // phpunit仕様
    // override
    protected function setUp()
    {
        parent::setUp();

        // 各テストメソッド実行前に
        // database/seeder/TestDataSeeder
        // のシーダーを実行する
        $this->artisan(
            'db:seed',
            ['--class' => 'TestDataSeeder']
        );
    }

データベースが絡むテスト

TODOリストの追加

  • api/customersにGETメソッドでアクセスできる
    • api/customersにGETメソッドでアクセスするとJSONが返却される
    • api/customersにGETメソッドで取得できる顧客情報のJSON形式は要件通りである
    • api/customersにGETメソッドで返却される顧客情報は2件である
  • ...

テスト追加

/tests/Feature/ReportTest.php

<?php

    /**
     * @test
     */
    public function api_customersにGETメソッドでアクセスするとJSONが返却される() {
        $response = $this->get('api/customers');
        $this->assertThat(
            $response->content(),
            $this->isJson()
        );
    }

bash

./vendor/bin/phpunit

無事死亡

There was 1 failure:

1) Tests\Feature\ReportTest::api_customersにGETメソッドでアクセスするとJSONが返却される
Failed asserting that an empty string is valid JSON.

仮実装で素早くテストを成功させる

とりあえず「JSONが返却される」ことを満足する

/routes/api.php

- Route::get('customers', function () {});
+ Route::get('customers', function () {
+         return response()->json();
+     });

bash

!!
OK (11 tests, 11 assertions)

最初のリファクタリング

これはリファクタと言うのだろうか

/routes/api.php

Route::get('customers', function () {
-       return response()->json();
+       return response()->json(\App\Customer::query()->get());
    });

bash

!!

エンバグのなきことを確認

OK (11 tests, 11 assertions)

返却値の内容を検証

/tests/Feature/ReportTest.php

<?php

    public function api_customersにGETメソッドで取得できる顧客情報のJSON形式は要件通りである()
    {
        $response = $this->get('api/customers');
        $customers = $response->json();

        // 先頭の要素がOKなら全customerデータOKとする
        $customer = $customers[0];
        // 所定のキーのみが過不足なくあること
        $this->assertSame(
            [
                'id',
                'name',
            ],
            array_keys($customer));
    }

bash

!!

無事死亡

There was 1 failure:

1) Tests\Feature\ReportTest::api_customersにGETメソッドで取得できる顧客情報のJSON形式は要件通りである
Failed asserting that Array &0 (
    0 => 'id'
    1 => 'name'
    2 => 'created_at'
    3 => 'updated_at'
) is identical to Array &0 (
    0 => 'id'
    1 => 'name'
).

routes/api.php

Route::get('customers', function () {
-       return response()->json(\App\Customer::query()->get());
+       return response()->json(\App\Customer::query()->select('id', 'name')->get());
    });

bash

!!

pass

OK (12 tests, 12 assertions)

成功が分かっているテストの追加

  • api/customersにGETメソッドで返却される顧客情報は2件である
  • もう動いている部分もテストを書く
<?php

    public function api_customersにGETメソッドで返却される顧客情報は2件である()
    {
        $response = $this->get('api/customers');
        $response->assertJsonCount(2);
    }

bash

!!

ちゃんと件数が増えてますね(12->13)

OK (13 tests, 13 assertions)

データ追加の検証

  • GETおわり
  • つぎPOST

TODOリスト追加

  • api/customersにGETメソッドでアクセスできる
  • api/customersにPOSTメソッドでアクセスできる
    • api/customersに顧客名をPOSTするとcustomersテーブルにそのデータが追加される

テスト書く

<?php

    public function api_customersに顧客名をPOSTするとcustomersテーブルにそのデータが追加される()
    {
        $params = [
            'name' => '顧客名',
        ];

        $this->postJson('api/customers', $params);
        $this->assertDatabaseHas('customers', $params);
    }

bash

./vendor/bin/phpunit

無事死亡

There was 1 failure:

1) Tests\Feature\ReportTest::api_customersに顧客名をPOSTするとcustomersテーブルにそのデータが追加される
Failed asserting that a row in the table [customers] matches the attributes {
    "name": "\u9867\u5ba2\u540d"
}.

Found: [
    {
        "id": 11,
        "name": "\u6709\u9650\u4f1a\u793e \u9752\u5c71",
        "created_at": "2019-01-14 11:50:55",
        "updated_at": "2019-01-14 11:50:55"
    },
    {
        "id": 12,
        "name": "\u682a\u5f0f\u4f1a\u793e \u9234\u6728",
        "created_at": "2019-01-14 11:50:55",
        "updated_at": "2019-01-14 11:50:55"
    }
].

実装

/routes/api.php

- Route::post('customers', function () {});
+ Route::post('customers', function (\Illuminate\Http\Request $request) {
+         $customer = new \App\Customer();
+         $customer->name = $request->json('name');
+         $customer->save();
+     });

bash

!!

今まで通っていた別のテストが通らなくなった!

There was 1 failure:

1) Tests\Feature\ReportTest::api_customersにPOSTメソッドでアクセスできる
Expected status code 200 but received 500.
Failed asserting that false is true.

既存のテストの修正

/storage/logs/larave.log

[2019-01-14 11:56:23] testing.ERROR: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name' cannot be null (SQL: insert into \`customers\` (\`name\`, \`updated_at\`, \`created_at\`) values (, 2019-01-14 11:56:22, 2019-01-14 11:56:22)) {"exception":"[object] (Illuminate\\Database\\QueryException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name' cannot be null (SQL: insert into \`customers\` (\`name\`, \`updated_at\`, \`created_at\`) values (, 2019-01-14 11:56:22, 2019-01-14 11:56:22)) at /home/vagrant/code/tdd_sample/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664, Doctrine\\DBAL\\Driver\\PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name' cannot be null at /home/vagrant/code/tdd_sample/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:119, PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name' cannot be null at /home/vagrant/code/tdd_sample/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:117)

抜粋

Integrity constraint violation: 1048 Column 'name' cannot be null

/routes/api.php の死んでるところ

<?php

Route::post('customers', function (\Illuminate\Http\Request $request) {
        $customer = new \App\Customer();
        $customer->name = $request->json('name');  // POSTするJSONが空だと、$request->json('name')がnullになる
        $customer->save();
    });

テストを修正する

    public function api_customersにPOSTメソッドでアクセスできる()
    {
-       $response = $this->post('api/customers');
+       $customer = [
+           'name' => 'customer_name',
+       ];
+       $response = $this->postJson(
+           'api/customers',
+           $customer
+       );

        $response->assertStatus(200);
    }

bash

!!
OK (14 tests, 14 assertions)

バリデーションテスト

TODO追加

  • パラメータ不足時のケースを考慮できていなかった
    • api/customersにPOSTメソッドでアクセスできる
      • POST api/customersにnameが含まれない場合は422 Unprocessable entityが返却される
      • POST api/customersのnameが空の場合は422 Unprocessable entityが返却される

テスト実装

<?php
    /**
     * @test
     */
    public function POST_api_customersにnameが含まれない場合は422_Unprocessable_entityが返却される()
    {

        $params = [];
        $response = $this->postJson('api/customers', $params);

        $response->assertStatus(\Illuminate\Http\Response::HTTP_UNPROCESSABLE_ENTITY);
    }

    /**
     * @test
     */
    public function POST_api_customersのnameが空の場合は422_Unprocessable_entityが返却される()
    {
        $params = ['name' => ''];
        $response = $this->postJson('api/customers', $params);
        
        $response->assertStatus(\Illuminate\Http\Response::HTTP_UNPROCESSABLE_ENTITY);
    }

bash

!!

無事死亡

There were 2 failures:

1) Tests\Feature\ReportTest::POST_api_customersにnameが含まれない場合は422_Unprocessable_entityが返却される
Expected status code 422 but received 500.
Failed asserting that false is true.

2) Tests\Feature\ReportTest::POST_api_customersのnameが空の場合は422_Unprocessable_entityが返却される
Expected status code 422 but received 500.
Failed asserting that false is true.

仮実装

Route::post('customers', function (\Illuminate\Http\Request $request) {
+       $customer_name = $request->json('name');
+       if (!$customer_name) {
+           return response()->json(
+               [],
+               \Illuminate\Http\Response::HTTP_UNPROCESSABLE_ENTITY
+           );
+       }
        $customer = new \App\Customer();
-       $customer->name = $request->json('name');
+       $customer->name = $customer_name;
        $customer->save();
    });

bash

!!
OK (16 tests, 16 assertions)

リファクタリングユースケース

そろそろコントローラを使う

つくる

bash

php artisan make:controller ApiController

/routes/api.phpの中身を移植

/routes/api.php

- Route::get('customers', function () {
-         return response()->json(\App\Customer::query()->select('id', 'name')->get());
-     });
- Route::post('customers', function (\Illuminate\Http\Request $request) {
-         $customer_name = $request->json('name');
-         if (!$customer_name) {
-             return response()->json(
-                 [],
-                 \Illuminate\Http\Response::HTTP_UNPROCESSABLE_ENTITY
-             );
-         }
-         $customer = new \App\Customer();
-         $customer->name = $customer_name;
-         $customer->save();
-     });
+ Route::get('customers', 'ApiController@getCustomers');
+ Route::post('customers', 'ApiController@postCustomers');

/app/Http/Controllers/ApiController.php

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ApiController extends Controller
{
    public function getCustomers(): \Illuminate\Http\JsonResponse
    {
        return response()->json(\App\Customer::query()->select('id', 'name')->get());
    }

    public function postCustomers(Request $request): \Illuminate\Http\JsonResponse
    {
        $customer_name = $request->json('name');
        if (!$customer_name) {
            return response()->json(
                [],
                \Illuminate\Http\Response::HTTP_UNPROCESSABLE_ENTITY
            );
        }
        $customer = new \App\Customer();
        $customer->name = $customer_name;
        $customer->save();

        // 厳格型検査を有効にすると、これが必要になる
        return response()->json(
            [],
            \Illuminate\Http\Response::HTTP_OK
        );
    }

/* ... */

bash

./vendor/bin/phpunit

エンバグのなきことを確認

OK (16 tests, 16 assertions)

フレームワークの標準に寄せていくリファクタリング - 1

  • 自前実装をフレームワーク標準機能で置換する
    • 「きれいな実装」に近づける
    • 将来の良好なメンテナンス性

せっかくなのでFormRequestを使ってみた

bash

php artisan make:request PostCustomersRequest

tree app/Http/Requests/
app/Http/Requests/
└── PostCustomersRequest.php

/app/Http/Requests/PostCustomersRequest.php

<?php

declare(strict_types=1);

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostCustomersRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        // 認可はない
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required',
        ];
    }
}

/app/Http/Controllers/ApiController.php

-   public function postCustomers(Request $request): \Illuminate\Http\JsonResponse
+   public function postCustomers(\App\Http\Requests\PostCustomersRequest $request): \Illuminate\Http\JsonResponse
    {
-       if (!$customer_name) {
-           return response()->json(
-               [],
-               \Illuminate\Http\Response::HTTP_UNPROCESSABLE_ENTITY
-           );
-       }

        $customer = new \App\Customer();
-       $customer->name = $customer_name;
+       $customer->name = $request->json('name');
        $customer->save();

        // 厳格型検査を有効にすると、これが必要になる
        return response()->json(
            [],
            \Illuminate\Http\Response::HTTP_OK
        );
    }

正確なテストが書けないときの対処法

  • 標準のバリデーションに失敗した場合、どんなレスポンスが返るのかわからない

とりあえず空配列が返ってくるものとしてテストを書いてみる

ReportTest.php

<?php

    public function POST_api_customersのエラーレスポンスの確認()
    {
        $params = ['name' => ''];
        $response = $this->postJson('api/customers', $params);

        // ここが分からない
        // とりあえず空にしてみる
        $error_response = [];
        $response->assertExactJson($error_response);
    }

bash

./vendor/bin/phpunit

そういう仕様でしたか

1) Tests\Feature\ReportTest::POST_api_customersのエラーレスポンスの確認
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'[]'
+'{"errors":{"name":["The name field is required."]},"message":"The given data was invalid."}'

標準のバリデーションの失敗メッセージの仕様をテストに盛り込む

ReportTest.php

-       // ここが分からない
-       // とりあえず空にしてみる
-       $error_response = [];
-       $response->assertExactJson($error_response);
+       // Laravel標準エラーメッセージ
+       $error_response = [
+           "errors" => [
+               "name" => [
+                   "The name field is required.",
+               ]
+           ],
+           "message" => "The given data was invalid.",
+       ];

bash

!!
OK (17 tests, 17 assertions)

エラー詳細メッセージを変えてみる

テスト

ReportTest.php

        // Laravel標準エラーメッセージ
        $error_response = [
            "errors" => [
                "name" => [
-                   "The name field is required.",
+                   "name は必須項目です",
                ]
            ],
            "message" => "The given data was invalid.",
        ];

bash

!!

無事死亡

There was 1 failure:

1) Tests\Feature\ReportTest::POST_api_customersのエラーレスポンスの確認
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'{"errors":{"name":["name \u306f\u5fc5\u9808\u9805\u76ee\u3067\u3059"]},"message":"The given data was invalid."}'
+'{"errors":{"name":["The name field is required."]},"message":"The given data was invalid."}'

実装

FormRequestmessages()をoverrideすればいいみたいですよ

Illuminate/Foundation/Http/FormRequest.php

<?php

    /**
    * Get the validator instance for the request.
    *
    * @return \Illuminate\Validation\Validator
    */
    protected function getValidatorInstance()
    {
        $factory = $this->container->make('Illuminate\Validation\Factory');
        if (method_exists($this, 'validator'))
        {
            return $this->container->call([$this, 'validator'], compact('factory'));
        }
        return $factory->make(
            $this->all(), $this->container->call([$this, 'rules']), $this->messages(), $this->attributes()
        );
    }

/app/Http/Requests/PostCustomersRequest.php

+   public function messages()
+   {
+       return [
+           'name.required' => 'name は必須項目です',
+       ];
+   }

bash

!!
OK (17 tests, 17 assertions)

フレームワークの標準に寄せていくリファクタリング - 2

/resources/lang/ja/validation.php使え、という話

/app/Http/Requests/PostCustomersRequest.php

-  public function messages()
-  {
-      return [
-          'name.required' => 'name は必須項目です',
-      ];
-  }

サービスクラスへの分離

<?php

    public function getCustomers(): \Illuminate\Http\JsonResponse
    {
        return response()->json(\App\Customer::query()->select('id', 'name')->get());
    }
  • ↑こういうのやめませんか、という話
    • 将来的な機能拡張やメンテナンス性
    • サービスクラスのUnit Testを書けるようになる

実装の移植

  • 呼び出し側から書くのがコツ
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\PostCustomersRequest;
use Illuminate\Http\JsonResponse;
use App\Services\CustomerService;

class ApiController extends Controller
{
    public function getCustomers(
        CustomerService $customer_service
    ): JsonResponse
    {
        return response()->json($customer_service->getCustomers());
    }

    public function postCustomers(
        PostCustomersRequest $request,
        CustomerService $customer_service
    ): JsonResponse
    {
        $customer_service->addCustomer(
            $request->json('name')
        );

        // 厳格型検査を有効にすると、これが必要になる
        return response()->json(
            [],
            \Illuminate\Http\Response::HTTP_OK
        );
    }
/* ... */
}

/app/Services/CustomerService.php

<?php

declare(strict_types=1);

namespace App\Services;


class CustomerService
{
    public function getCustomers()
    {
        return \App\Customer::query()->select('id', 'name')->get();
    }

    public function addCustomer($name)
    {
        $customer = new \App\Customer();
        $customer->name = $name;
        $customer->save();
    }
}

bash

./vendor/bin/phpunit
OK (17 tests, 17 assertions)

まだ実装すんでないけど頑張ってね

  • やりません