この本の輪読会
冗談みたいに長いんですけど
テスト駆動開発(TDD: Test-Driven Development)とは
-
Kent Beck氏が考案した手法
- XP: Extreme Programming の考案者
- 「きれいな実装」で「きちんと動作する」ソースコードを目指す
-
いきなり「きれいな実装」を目指さない
- きちんと動作することを確認するためのテストを書く
-
実装前に、テストが失敗することを確認する
-
【補】ちゃんとテストが実施されていることを確認
@test
アノテーションつけ忘れで実施が漏れることが割とある
-
-
きちんと動作する実装をできるだけ素早く行う
- 汚くていい
- テストが成功することを確認する
- テストが失敗しないことを確認しながら、きれいな実装を目指してリファクタリングする
コツはできるだけ小さく
- 最重要 by 執筆陣
-
すべてにつけてそう
- テスト作成時
- 最初の実装にかける時間
- リファクタ中のテスト実行間隔
-
効能
- 集中力が途切れにくくなる
- 開発作業にリズムが生まれる
本章のねらい
-
以下を体験する
-
安心感
- 必要な機能が満たされていることが、テストにより保証される
-
高揚感
- 短いサイクルで集中してリズムよく開発
-
達成感
- コードが徐々にきれいになっていく
-
サンプルアプリケーション仕様
- 略(pp.467-468)
-
モバイルアプリケーションに利用されるAPI
- モバイルアプリケーションそのものは作らない
データベース仕様
- 略(pp.468-470)
APIエンドポイント
- 略(pp.470-471)
APIエンドポイントの作成
アプリケーションの作成・事前準備
環境
- Homestead
on Vagrant 2.2.3
on VirtualBox 6
プロジェクト作成
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.*"
-
composerが遅いので早くしてからプロジェクトを作る
- 2分くらいで済む
【補】単一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/customersにGETメソッドでアクセスできる
- api/customersにPOSTメソッドでアクセスできる
- …(略)
- 最初から完璧じゃなくていい
テストファイルの作成
- エンドポイントへのアクセス確認 = 機能テストを作成
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)
-
-
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);
});
});
}
マイグレーション・シーディング
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)
リファクタリングユースケース
-
大胆なリファクタリングはTDDの醍醐味
- テストがあるから安心して実施できる
そろそろコントローラを使う
つくる
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."}'
実装
`FormRequest`の`messages()`を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を書けるようになる
-
ビジネスロジックに関するテストはサービスクラスに対して書く
- JSONのフォーマットチェックとか
-
コントローラはあくまでレスポンスをチェックするだけでよくなる
- 200とか422とか
-
実装の移植
- 呼び出し側から書くのがコツ
<?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)
まだ実装すんでないけど頑張ってね
- やりません