Laravelでテストコードを書いてみる。
てか、テストコード自体書いたことない。
試行錯誤で進めていくが、やったことを忘れそうなのでとりあえずやったことを書いていくよ。
Lravelのバージョンは7.13.0だ。
#1.どんなテストをするか考える
今回はAPIのテストをするけど、いったいどんなテストをする気だ、俺?
今回テストするAPIの仕様はざっくり下記の通り。
・JSONで受け取ったデータを、テーブルに登録する
・リクエストされたキーの内容をJSONで返す
これらの機能を確認するのに、どんなテストをすればいいか?
・正常ケースのテスト
・validationでエラーになったときのテスト
・指定されたキーが存在しなかったときのテスト
そうすると、テストをするうえで準備しなければならないことは。
・DBにデータを登録する。
・正常ケースのテストケースを作る
・validationのエラーケースを作る
・テーブルに存在しないデータのエラーケースを作る
こんな感じで、やってみよう。
#2.テストクラスを作る
何はともあれ、テストクラスを作ってみる。
php artisan make:test ApiControllerTest
今回はAPIのテスト用なので、ユニットではなくコントローラーのテストをするから、--unitオプションはつけないで実施。
すると、下記クラスが/tests/Featuerの下に作成される。
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class ReceiptControllerTest extends TestCase
{
/**
* A basic feature test example.
*
* @return void
*/
public function testExample()
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
これに、テストのいろんなことを仕込んでいく。
#2.テスト用の設定を仕込む
テストを行う場合、一般的にはテスト用のDBを使用する。今回もテスト用のDBを作りたいけど、テストDB用のmigrationを作ったりするのは二重管理になったりして面倒。
そこで、テスト用のDBを作成する。
##2.1 テスト用DBを作成する
テスト用DBは自動的には作られないので、手作成する。
> mysqladmin create api_test
これだけでDBは作成されます。中身は空だけどね。
##2.2 .env.testingの設定
.envを基にして、.env.testingを作成する。
今回はテスト用のDBを使用するように設定するので、主にDB周りの設定を変更する。
~ 省略 ~
APP_ENV=testing ←testingに変更
APP_KEY= ←空にする
~ 省略 ~
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=api_test ←テスト用DBに変更
DB_USERNAME=root
DB_PASSWORD=
~ 省略 ~
DB自体が違うとか、接続方法変えるとか、IP変えるとか、それぞれの事情に合わせてDB_~の設定を変更してね。今回はDB_DATABASEを変更しました。
##2.3 phpunitの変更
デフォルトで入っていたphpunit.xmlは下記の通り。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>
これをテスト用に修正する。
・<server ~>は<env ~>に変更する。
・DB_CONNECTIONはmysqlに変更する。
・DB_DATABASEをテスト用DBのapi_testに変更する。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="DB_CONNECTION" value="mysql"/>
<env name="DB_DATABASE" value="api_test"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>
変更した後、AP KEYをgenerateする。
php artisan key:generate --env=testing
実行すると、.env.testingのAP_KEYに値が設定される。
//php artisan key:generate --env=testing実行後
APP_KEY=base64:lrXtt+RXURwNgEcYa6RWUSkJmYYJh+Rcv12Cj86pO+w=
#3. テストメソッドを実装する
さて、テストクラスにテストメソッドを仕込むのですが。
テストメソッドはメソッド名の先頭に「test」をつけるか、メソッドのdocコメントに@testアノテーションをつけると。テストメソッドと認識されます。
テストメソッドはテストごとに作成していきます。
あと、テスト実行ごとにDBの初期化を行うため、RefreshDatabaseトレイトを使用します。
##3.1正常ケース
###3.1.1メソッドを作る
まずはJSONデータをテーブルに登録するケースを作ります。
やりたいことはふたつ。
・JSONの中に指定したキーの存在チェックを行っているので、その分のデータを仕込んでおく。
・JSONでデータを送る。
作成したメソッドがこちら
public function testStore()
{
// companyテーブルにデータを仕込む
\App\Company::create([
'name' => 'Test corp'
]);
//JSONデータ作成
$json = [
'company_id' => '1',
'total_tax' => '100',
'total_fee' => '1000'
];
$response = $this->postjson(route('api.store'),$json);
$response->assertStatus(200)
->assertJsonFragment([
'status' => 0,
'store_id' => 1
]);
}
データはとりあえず1件あればいいので、普通にcreateしています。
大量データが欲しいときはFasade使ったりすればよし。
postするデータがjsonなので、postjsonを使っています。
普通の配列などのデータを渡す場合はpostでOKです。
テストする内容はふたつ目の$responseに書きます。
今回のポイントは、
・HTTPステータスが「200」であること
・プログラムから返される「status」が「0」であること
・プログラムから返される「store_id」が登録時のid(今回は1件目なので「1」)であること
を確認します。
###3.1.2 実行する
メソッドを作成したら、testを実行してみます。
テストはhomesteadにsshで接続して、プロジェクトのディレクトリに移動した後、下記のコマンドで実行します。
$ ./vendor/bin/phpunit
ちなみに、なぜかWindowsでVS Codeのterminalで>vendor/bin/phpunitで実行したら、メッセージが文字化けしてエラーになった。現状原因不明です。
なので実行はlinux下で行っています。
もろもろ上手く設定できていれば、正常ケースなので正常なメッセージが返ってきます。こんな感じ。
$ ./vendor/bin/phpunit
HPUnit 8.5.5 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 2.87 seconds, Memory: 24.00 MB
OK (3 tests, 5 assertions)
チェックしている項目に、想定と違う値が返ってきたときは、こんな風に帰ってきます。今回はstatusが9で返ってきた場合の表示例です。
$ ./vendor/bin/phpunit
PHPUnit 8.5.5 by Sebastian Bergmann and contributors.
..F 3 / 3 (100%)
Time: 2.79 seconds, Memory: 24.00 MB
There was 1 failure:
1) Tests\Feature\ReceiptControllerTest::testStore
Unable to find JSON fragment:
[{"status":0}]
within
[{"receipt_id":1,"status":9}].
Failed asserting that false is true.
/home/vagrant/code/restReceipt/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:570
/home/vagrant/code/restReceipt/tests/Feature/ReceiptControllerTest.php:59
FAILURES!
Tests: 3, Assertions: 5, Failures: 1.
statusが0って言ってるのに、違うのが返ってきたよ、と。
そうなったらどこかバグったということなので、正しくなるよう修正してね。
###3.1.3 showのテストメソッドも作ってみる
同様に、showメソッド用のテストメソッドも作ってみる。
public function testShow()
{
//データ仕込み
//comapny table
\App\Company::create([
'name' => 'Test corp'
]);
$company = DB::table('company')
->select('id')
->orderBy('id','DESC')
->first();
$company_id = $company->id;
//receipt table
\App\Receipt::create([
'company_id' => $company_id,
'total_tax' => 100,
'total_fee' => 1000
]);
// テスト
$response = $this->get(route('receipt.show',['receipt' => $receipt_id]));
$response->assertStatus(200)
->assertExactJson([
'status' => 0,
'receipt_id' => $receipt_id,
'company_name' => 'Test corp',
'total_tax' => 100,
'total_fee' => 1000
]);
}
showは照会(渡された値でreceipt_idを検索して表示)なので、データを仕込むSQLを先頭に書いてます。このやり方は正しいのか?違ってたら誰か教えてくれ。
で、登録した内容がapiのリクエストでちゃんと返ってくるよね、という確認をしています。
assertExactJsonは、JSONの内容が完全一致していることを検証します。
これでテストを実行して、OKだとこんな感じの表示になります。
$ ./vendor/bin/phpunit
PHPUnit 8.5.5 by Sebastian Bergmann and contributors.
.... 4 / 4 (100%)
Time: 3.82 seconds, Memory: 24.00 MB
OK (4 tests, 7 assertions)
「.」が4つになりました。
ちなみに値が違っている場合は、こんな感じでリターンされます。
$ ./vendor/bin/phpunit
PHPUnit 8.5.5 by Sebastian Bergmann and contributors.
...F 4 / 4 (100%)
Time: 4.06 seconds, Memory: 24.00 MB
There was 1 failure:
1) Tests\Feature\ReceiptControllerTest::testShow
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'{"company_name":"Test corp","receipt_id":1,"status":0,"total_fee":9999,"total_tax":100}'
+'{"company_name":"Test corp","receipt_id":1,"status":0,"total_fee":1000,"total_tax":100}'
/home/vagrant/code/restReceipt/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:545
/home/vagrant/code/restReceipt/tests/Feature/ReceiptControllerTest.php:122
FAILURES!
Tests: 4, Assertions: 7, Failures: 1.
なんかよさげに出来ました。
##3.2 エラーケース
validationでエラーにするケースも、ちゃんとエラーになることをテストしたい。
company_idがテーブルに存在しないケースと、total_taxが数字じゃないケースのテストを仕込んでみる。
public function testexists_company_id()
{
//JSONデータ作成
$data = [
'company_id' => '99999',
~ 省略 ~
];
$response = $this->postjson(route('receipt.store'),$data);
$response->assertStatus(400)
->assertJsonFragment([
'status' => 400,
]);
}
public function testnumeric_total_tax()
{
//JSONデータ作成
$data = [
'company_id' => '1',
'total_tax' => '10z',
~ 省略 ~
];
$response = $this->postjson(route('receipt.store'),$data);
$response->assertStatus(400)
->assertJsonFragment([
'status' => 400,
]);
}
正常ケースのJSONをエラーになるよう変更する。
ステータスを400で返すように設定しているので、assertStatusを400にする。
エラーメッセージなど、他にも確認したいデータがあれば、assertJsonFragmentの中に書いてね。ちなみにassertJsonFragmentは、この中井書いてあるものが含まれていればOKなので、完全一致で確認したいときはassertExactJsonでチェックしてね。
で、これを実行するとこんな感じになる。
$ ./vendor/bin/phpunit
PHPUnit 8.5.5 by Sebastian Bergmann and contributors.
...... 6 / 6 (100%)
Time: 5.43 seconds, Memory: 26.00 MB
OK (6 tests, 11 assertions)
エラーケースが正常にチェックされているので、全部OKって言われるよ。
こんな感じで仕込んでおけば、プログラムを変更しても一発で既存ケースのテストができるから、デグることもなくなるね。
面倒がらずに書いておくのが吉。
目指せノンデグ人生!!
それでは。