今回は、異常終了や予期しないエラーへの対策を施し、テストを行っていきます。
現状、予期しないエラー発生時にはどうなるか?
存在しないタスクを要求するとどのようになるかを確かめます。下記にアクセスしてください。
上記にアクセスすると表示される画面は下記になります。
エラーが表示されてしまいます。
これは、TaskController
のshow
メソッドの下記のコードのためです。
$task = Task::Find($id);
引数に指定したid
でFind
を行うのですが、データベースに該当のレコードがないため$task
は null となります。
そのため$taskの値を表示する箇所でエラーとなります。
エラーを防ぐためには、$task
が null の場合、404エラー(Not Found)を表示する処理が必要となります。
予期しないエラー発生時の処理
Laravel では、検索が失敗する事を考慮したFindOrFail
という便利なメソッドがあります。
Task::Find($id)
を下記のように書き換えます。
$task = Task::FindOrFail($id);
書き換え後に再度、http://localhost/tasks/999 にアクセスしてください。
シンプルにエラーコード 404 とメッセージが表示されています。
このページの情報だけでは、ユーザーは何をすればよいかがわからないためもう少し手を加えるべきですが、今は残課題としてリストアップしておき、このままにしておきましょう。
Laravel でのエラー処理の公式ドキュメントの翻訳は下記になります。
https://readouble.com/laravel/7.x/ja/errors.html
予期しないエラーが発生するケース
検索時の予期しないエラーを含めそのほかにも考えられるものを列挙しました。
- 新規登録ができない
- 指定したデータが存在しない
- 指定したデータが更新できない
- 指定したデータが削除できない
メソッド単位に実装を改善
TaskController
のメソッド別に予期しないエラーが発生する箇所を修正していきます。
create
該当なし。
store
$task->save()
時に失敗した場合の処理を記述します。
//$task->save();
$result = $task->save();
if (!$result) {
abort(422);
}
save() メソッドの戻り値は、saveに成功したときはTRUE、失敗したときはFALESEになります。
edit
Task::Find
をTask::FindOrFail
に変更します。
//$task = Task::Find($id);
$task = Task::FindOrFail($id);
update
Task::Find
をTask::FindOrFail
に変更します。
//$task = Task::Find($id);
$task = Task::FindOrFail($id);
$task->save()
時に失敗した場合の処理を記述します。
//$task->save()
if (!$task->save()) {
abort(422, 'fail update.');
}
updateStatus
update
時と同様の修正をします。
//$task = Task::Find($id);
$task = Task::FindOrFail($id);
$task->save()
時に失敗した場合の処理を記述します。
//$task->save()
if (!$task->save()) {
abort(422, 'fail update.');
}
destroy
destroy
メソッドは、削除件数が戻り値として返される。
そのため削除件数が 1 件未満の場合は、削除対象がないか、削除に失敗したのどちらかのためエラーとする。
//Task::destroy($id);
$deletedRows = Task::destroy($id);
if ($deletedRows < 1) {
abort(422, 'fail destroy.');
}
テスト
対象が存在しない場合のテストと保存や更新などのデータベースのレコード操作の失敗のテストに分けます。
存在しない場合のテスト実装
php artisan make:test NotFoundlTest
生成後にuse RefreshDatabase
など必要なものを追記します。
showに対してのテスト
show
メソッドに対してのテストを実装します。
public function testShow()
{
$response = $this->get(route('task.edit', [ 'id' => 999 ]));
$response->assertStatus(404);
}
updateに対して更新する対象のデータが存在しない
update
メソッドに対して、更新する対象のデータが存在しない場合のテストを実装します。
public function testUpdateShow()
{
$data = [
'status' => 1,
'title' => "title",
'description' => "wゑエ😀𩸽"
];
$response = $this->put(route('task.update', [ 'id' => 999 ]), $data);
$response->assertStatus(404);
}
updateStatusに対して更新する対象のデータが存在しない
updateStatus
メソッドに対して更新する対象のデータが存在しない場合のテストを実装します。
public function testUpdateStatusNotFound()
{
$response = $this->get(route('task.updateStatus', ['id' => 999,'afterstatus' => 4]));
$response->assertStatus(404);
}
destroyに対して削除する対象のデータが存在しない
destroy
メソッドに対して削除する対象のデータが存在しない場合のテストを実装します。
public function testDeleteShow()
{
$response = $this->delete(route('task.delete', [ 'id' => 999 ]));
$response->assertStatus(422);
}
レコード操作失敗のテスト実装
テストクラスを作成します。
php artisan make:test AbnormalTest
生成後にuse RefreshDatabase
など必要なものを追記します。
<?php
namespace Tests\Feature;
use Mockery;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Task;
class AbnormalTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
}
public function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
}
さらに、上記のようにuse Mockery
を追加してください。
モックを作成するためのツールのMockery
を使用するためにインポートを行います。
Mockery
については詳細は Laravel の公式ドキュメントの翻訳を参照してください。
https://readouble.com/mockery/1.0/ja/index.html
storeに対してのテスト
store
メソッドに対してのテストを実装します。
public function testStore()
{
$mock = Mockery::mock('overload:App\Task');
$mock->shouldReceive('fill')->once()->andReturn(true);
$mock->shouldReceive('save')->once()->andReturn(false);
$data = [
'status' => 1,
'title' => "title",
'description' => "description"
];
$response = $this->post('/tasks', $data);
$response->assertStatus(422);
}
ポイントは、1 行目でモックを生成しているところです。
テスト対象のstore
メソッドは、メソッド内で、App\Task
のオブジェクトを生成しています。
下記の$task = new Task()
です。
public function store(Request $request)
{
$request->validate([
'title' => ['max:20','required'],
'description' => ['max:200']
]);
$task = new Task();
$task->fill($request->all());
$task->status = 1;
$result = $task->save();
if (!$result) {
abort(422);
}
return redirect(route('home'));
}
その後に、$task
オブジェクトに対して、fill()
、save()
メソッドを呼んでいます。
今、取り組もうとしているのが異常系のテストですので、save()
メソッドを呼んだときの戻り値が false にならないとテストが行えません。
こういうときに、モックを使用することで、常にfalse を戻せます。
テストコードの下記行で、save
が呼ばれたときに戻り値として false を返すように設定しています。
$mock->shouldReceive('save')->once()->andReturn(false);
結果、abort(422)
が常に実行されるようなります。
updateに対してのテスト
update
メソッドに対して更新に失敗するテストを実装します。
public function testUpdate()
{
$taskmodel = new class {
public function fill($array)
{
return true;
}
public function save()
{
return false;
}
};
$mock = Mockery::mock('overload:App\Task');
$mock->shouldReceive('FindOrFail')->once()->andReturn($taskmodel);
$data = [
'status' => 1,
'title' => "title",
'description' => "wゑエ😀𩸽"
];
$response = $this->put("/tasks/1", $data);
$response->assertStatus(422);
}
ポイントは、FindOrFail
メソッドが呼ばれた際に、モッククラスを返却するようにしている箇所です。
モッククラスは、save
メソッドの戻り値として false を返す無名クラスです。
updateStatusに対してのテスト
updateStatus
メソッドに対して更新に失敗するテストを実装します。
public function testUpdateStatus()
{
$taskmodel = new class {
public $status;
public function save()
{
return false;
}
};
$mock = Mockery::mock('overload:App\Task');
$mock->shouldReceive('FindOrFail')->once()->andReturn($taskmodel);
$response = $this->get(route('task.updateStatus', ['id' => 1,'afterstatus' => 4]));
$response->assertStatus(422);
}
destroyに対してのテスト
destroy
メソッドに対して削除に失敗するテストを実装します。
public function testDelete()
{
$mock = Mockery::mock('overload:App\Task');
$mock->shouldReceive('destroy')->once()->andReturn(0);
$response = $this->delete(route('task.delete', [ 'id' => 999 ]));
$response->assertStatus(422);
}
overload について
ところで、モックを作る際に、必ずoverload
というものを指定しています。
$mock = Mockery::mock('overload:App\Task');
テスト対象のstore
メソッド内でnew
しているオブジェクトを置き換える場合は、overload
を指定する必要があります。
overload
は、非常に便利なのですがテスト対象のコードにて使用されるメソッドすべてを実装する必要があります。
例えば、前述の「updateに対してのテスト」のモックでは、saveメソッドに加えて、fillメソッドも実装しています。
これを回避するためには、App\Task
のオブジェクトの生成は Laravel に任せて、コントローラ生成時か、メソッド呼び出し時に自動で渡されるようにコードを書くようにします。(コンストラクタインジェクション or メソッドインジェクション or ルートモデルバインディング)
その場合は、overload
は不要で、下記のようにモックを生成できます。
$mock = Mockery::mock('App\Task')
そして、makePartial()にてパーシャルモックを生成できます。
$mock = Mockery::mock('App\Task')->makePartial()
パーシャルモックは、元のクラスのメソッド、プロパティをコピーしたインスタンスを生成します。 すなわち、モックしたいメソッド以外の実装が不要になります。
overload
についての公式の詳しい解説は下記にあります。
http://docs.mockery.io/en/latest/cookbook/mocking_hard_dependencies.html
テスト実行
下記のコマンドでテストを実行します。
$ ./vendor/bin/phpunit tests/Feature/NotFoundlTest.php --testdox
$ ./vendor/bin/phpunit tests/Feature/AbnormalTest.php --testdox
一部のテストのみ行う場合は下記のように--filter
をつけます。
$ ./vendor/bin/phpunit tests/Feature/AbnormalTest.php --filter 'testStore'
エラーの場合
テストコードを実行して、ステータスコードが 500 の失敗となった場合、setUp()
に下記を追加してください。
protected function setUp(): void
{
parent::setUp();
$this->withoutExceptionHandling(); // 追加
}
より詳しいエラーメッセージが表示されます。