こんなメソッドがあったとします。
(create使ってないのは気にしないでください..)
public function テスト対象メソッド($uuid, int $aiteId, \App\ChatRead $chatRead) { DB::transaction(function () use ($uuid, $aiteId, $chatRead) { $chat_room = $this->newInstance(); //略 $save = $chat_room->save(); // save 1つめ if ($save) { $chatRead_1 = $chatRead; //略 $chatRead_1->save(); //save 2つめ $chatRead_2 = $chatRead; //略 $chatRead_2->save(); //save 3つめ } }); }
中で3ヶ所 save() を使っています。
どこかでエラーが出たら3カ所のsaveが全部ロールバックするように、DB::transaction で括っています。
↑↑この「エラー時にロールバックされる状況」 をテストで確認したいとします。
テストコードをどう書くか考える
「エラー時にロールバックされる状況」を確認したいので、
/** * @test */ public function エラー時ロールバックされるかテスト() { // 1:saveメソッドでエラーを発生させる準備(モックやDB重複データなど) // 2:メソッド実行 // 3 : 例外が出たか・DBロールバックされたかアサート }
のような感じで書けばいいのかなぁと最初考えたのですが、↑のままではテストはうまくいきません。
1:saveメソッドでエラーを発生させる準備
のお陰でエラーが発生するので、2の段階で実行すると書いた通りテストが落ちてしまうためです。
例外テストで使える $this->expectException()
例外を発生させるテストなのに例外が出たらテストが落ちてしまう!ということで、ドキュメントにも載っている
$this->expectException();
を使ってみました。
例外が発生するよね?をアサートしてくれます。つまり2の段階でテストを実行してもエラーで落ちません!
- 普通のAssert文のように最後に書くのではなく、テスト冒頭に書く
- docコメントに書くこともできるけど $this->expectException() のほうが良いらしい(可読性??)
ということだけ頭に置いておくと、とても便利に使えそうです。
ドキュメントにはエラーメッセージやエラーコードのアサート用記述も載ってます。
$this->expectException()を使うとAssert文が使えない!
エラーでテストが落ちなくなったのは良いものの、「DBがロールバックされたかアサート」するために //3の部分に
$this->assertDatabaseMissing('chat_rooms', [ 'uuid' => $uuid, ]);
と書き込んでも、このアサート文はスルーされます。
テストがOKになるのでこのアサートも通ったように思ってしまうのですが、ddで確認すると //2 より後ろは通っていません。
テストメソッドは今こんな感じになっています。(関係ない箇所は省略)
/** * @test */ public function エラー時ロールバックされるかテスト() { // 例外が出るかアサート(冒頭に書く) $this->expectException(\Exception::class); // 1:saveメソッドでエラーを発生させる準備(モックやDB重複データなど) // 2:メソッド実行 // 3 : DBロールバックされたかアサート(★テストはここを通らない!!) $this->assertDatabaseMissing('chat_rooms', [ 'uuid' => $uuid, ]); }
ドキュメント読んでもその辺りの言及が無いようだったので、とても悩みました。
解決策:try catch を使う
- $this->expectExceptionを使うと最後のAssertが機能しない
- $this->expectExceptionを使わないとテスト自体がエラーで落ちる
この解決策は、stack overflow に載っていました!
色んなコメントがついているので詳しくはリンク先で読んでみてください。私の場合は$this->expectException() を削除し、try catchを使うことで良い感じにアサートすることが出来ました。
/** * @test */ public function エラー時ロールバックされるかテスト() { // 1:saveメソッドでエラーを発生させる準備(モックやDB重複データなど) try { $this->sut->テスト対象メソッド(); // 2:メソッド実行 } catch (\Exception $ex) { $this->assertStringContainsString("Duplicate entry", $ex->getMessage()); } $this->assertDatabaseMissing('chat_rooms', [ // 3 : DBロールバック確認 'uuid' => $uuid, ]); }
$this->assertDatabaseMissing
を try catch の外に置いたので、もし例外が発生せずDBがロールバックしなかった場合はアサート失敗となります。