Laravel 自動テスト時のエラーメッセージを抑制する
前書き
Laravel で phpunit を使った自動テストを行っていると、テストに失敗した際、デバッグに役立つエラーメッセージを表示してくれます。これはこれで便利なのですが、状況によっては、「そこまで要らないよ…」と思うこともあったりします。
特に1つ2つのテストを走らせる時はいいのですが、テスト全体を走らせる時などは、親切過ぎるエラーメッセージは、全体像を見渡すのに非常に不便だったりします。
そこで、エラーメッセージの出力を抑制する(簡易にする)方法を考えてみました。
以降は、Laravel10 を前提にしています。
Laravel9 の後半以降でも、似たようなやり方でできたりするのですが、今後は、Laravel10 が主流になるのと紙面の都合上、Laravel10 に限定させていただきました🙇♂️
また、今回ご紹介するやり方は、Laravel 本体のコードの変更により、いつ使えなくなるか分かりませんので、その辺含め、自己責任にてお願いします🙇♂️👍
本題に入る前に少し脇道
Laravel9 以前では、->assertSee() などでテストに失敗した際、該当する HTML が、ひたすら画面に出力されていました。でも実際は、ほぼ見ないんですよね。
こういう事もあってエラーメッセージを抑制する事を行い始めたのですが、Laravel10 (※1)では、これが改良され、HTML の全体ではなく、数行のみが表示されるようになりました。
以前のように、HTML 全体を表示したい場合は、-v オプションを付ければ表示されます。
(--verbose
の略なのですが、phpunit10 では、--verbose
オプションが削除されてしまい、こちらだとエラーになります…。まぁ、--verbose
自体が冗長だし🙃)
ただ、->assertSee() 以外でも、例外が起きた際などはエラーメッセージが大量に出力されますので、それらの出力も抑えたいところです。
(※1)より正確には、Laravel10 というよりかは、nunomaduro/collision Ver.7 での対応になっている為、Laravel9 などからバージョンアップして、Laravel10 になった場合で、nunomaduro/collision Ver.7 未満のままの場合は、Laravel9 時と同様になります。
本題
という事で本題です。まずは、エラーメッセージが普段通りに出た場合と抑制した場合のサンプルから。共に php artisan test の結果。(2つのテストで失敗する場合)
普段通り(長すぎるので、かなり省略)
taro ~/proj/temp (master)> php artisan test
PASS Tests\Unit\ExampleTest
✓ that true is true
FAIL Tests\Feature\ExampleTest
⨯ the application returns a successful response 0.11s
⨯ the application returns a successful response2 0.02s
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\ExampleTest > the application returns a successful response
Expected response status code [200] but received 500.
Failed asserting that 200 is identical to 500.
The following exception occurred during the last request:
Error: Call to undefined function hoge() in /home/path/temp/routes/web.php:22
Stack trace:
#0 /home/path/temp/vendor/laravel/framework/src/Illuminate/Routing/CallableDispatcher.php(40): Illuminate\Routing\RouteFileRegistrar->{closure}()
#1 /home/path/temp/vendor/laravel/framework/src/Illuminate/Routing/Route.php(237): Illuminate\Routing\CallableDispatcher->dispatch()
#2 /home/path/temp/vendor/laravel/framework/src/Illuminate/Routing/Route.php(208): Illuminate\Routing\Route->runCallable()
#3 /home/path/temp/vendor/laravel/framework/src/Illuminate/Routing/Router.php(798): Illuminate\Routing\Route->run()
#4 /home/path/temp/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\Routing\Router->Illuminate\Routing\{closure}()
#5 /home/path/temp/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(50): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
(~~~~~~~~~~ 長すぎるので略 ~~~~~~~~~~~)
#49 /home/path/temp/vendor/phpunit/phpunit/src/Framework/TestCase.php(448): PHPUnit\Framework\TestRunner->run()
#50 /home/path/temp/vendor/phpunit/phpunit/src/Framework/TestSuite.php(352): PHPUnit\Framework\TestCase->run()
#51 /home/path/temp/vendor/phpunit/phpunit/src/Framework/TestSuite.php(352): PHPUnit\Framework\TestSuite->run()
#52 /home/path/temp/vendor/phpunit/phpunit/src/Framework/TestSuite.php(352): PHPUnit\Framework\TestSuite->run()
#53 /home/path/temp/vendor/phpunit/phpunit/src/TextUI/TestRunner.php(63): PHPUnit\Framework\TestSuite->run()
#54 /home/path/temp/vendor/phpunit/phpunit/src/TextUI/Application.php(137): PHPUnit\TextUI\TestRunner->run()
#55 /home/path/temp/vendor/phpunit/phpunit/phpunit(90): PHPUnit\TextUI\Application->run()
#56 {main}
----------------------------------------------------------------------------------
Call to undefined function hoge()
at tests/Feature/ExampleTest.php:22
18▕ public function test_the_application_returns_a_successful_response(): void
19▕ {
20▕ $response = $this->get('/');
21▕
➜ 22▕ $response->assertStatus(200)
23▕ ->assertSee('hogehoge barbar');
24▕ }
25▕
26▕ /**
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\ExampleTest > the application returns a successful response2
Failed asserting that a row in the table [users] matches the attributes {
"name": "hogehoge barbar",
"email": "fksljflkef",
"email_verified_at": "lfefjelkfsjelfjk"
}.
Found: [
{
"name": "Dr. Edmund Fritsch I",
"email": "htrantow@example.org",
"email_verified_at": "2023-02-16 09:38:55"
}
].
at tests/Feature/ExampleTest.php:33
29▕ public function test_the_application_returns_a_successful_response2(): void
30▕ {
31▕ User::factory()->create();
32▕
➜ 33▕ $this->assertDatabaseHas(User::class, [
34▕ 'name' => 'hogehoge barbar',
35▕ 'email' => 'fksljflkef',
36▕ 'email_verified_at' => 'lfefjelkfsjelfjk',
37▕ ]);
Tests: 2 failed, 1 passed (3 assertions)
Duration: 0.16s
抑制した場合
taro ~/proj/temp (master)> php artisan test
PASS Tests\Unit\ExampleTest
✓ that true is true
FAIL Tests\Feature\ExampleTest
⨯ the application returns a successful response 0.11s
⨯ the application returns a successful response2 0.02s
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\ExampleTest > the application returns a successful response
Failed.
at tests/Feature/ExampleTest.php:22
18▕ public function test_the_application_returns_a_successful_response(): void
19▕ {
20▕ $response = $this->get('/');
21▕
➜ 22▕ $response->assertStatus(200)
23▕ ->assertSee('hogehoge barbar');
24▕ }
25▕
26▕ /**
1 tests/TestCase.php:43
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\ExampleTest > the application returns a successful response2
Failed.
at tests/Feature/ExampleTest.php:33
29▕ public function test_the_application_returns_a_successful_response2(): void
30▕ {
31▕ User::factory()->create();
32▕
➜ 33▕ $this->assertDatabaseHas(User::class, [
34▕ 'name' => 'hogehoge barbar',
35▕ 'email' => 'fksljflkef',
36▕ 'email_verified_at' => 'lfefjelkfsjelfjk',
37▕ ]);
1 tests/TestCase.php:43
Tests: 2 failed, 1 passed (3 assertions)
Duration: 0.16s
こんな感じです。
ちなみに、vendor/bin/phpunit で抑制した場合は、下記の感じ。
vendor/bin/phpunit
taro ~/proj/temp (master)> vendor/bin/phpunit
PHPUnit 10.0.7 by Sebastian Bergmann and contributors.
Runtime: PHP 8.2.0RC3
Configuration: /home/path/temp/phpunit.xml
.FF 3 / 3 (100%)
Time: 00:00.166, Memory: 34.50 MB
There were 2 failures:
1) Tests\Feature\ExampleTest::test_the_application_returns_a_successful_response
Failed.
/home/path/temp/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:123
/home/path/temp/tests/Feature/ExampleTest.php:22
/home/path/temp/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:173
/home/path/temp/tests/TestCase.php:43
2) Tests\Feature\ExampleTest::test_the_application_returns_a_successful_response2
Failed.
/home/path/temp/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php:28
/home/path/temp/tests/Feature/ExampleTest.php:33
/home/path/temp/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:173
/home/path/temp/tests/TestCase.php:43
FAILURES!
Tests: 3, Assertions: 3, Failures: 2.
抑制した方が全体像が見渡し易くなりますね。
で、私の場合、どんな時にエラーメッセージの出力を抑制したいかと言いますと、以下の3パターンのコマンドでテストを走らせた時(=テスト全体を走らせる時)です。
- vendor/bin/phpunit(オプション無し)
- php artisan test(オプション無し)
- php artisan test --parallel (--parallel オプション付き)
上記のいずれかで呼ばれた際は、エラーメッセージの出力を抑制することにしました。という事で、下記がそのコード。
tests/TestCase.php に記載します。
protected function runTest(): mixed
{
$suppress = function () {
return $_SERVER['argc'] === 1 ||
($_SERVER['argv'][0] === 'vendor/phpunit/phpunit/phpunit' && $_SERVER['argc'] === 3) ||
str_contains($_SERVER['argv'][0], 'paratest');
};
if (! $suppress()) {
return parent::runTest();
}
$result = null;
try {
$result = parent::runTest();
} catch (\Throwable $e) {
if (! is_null(static::$latestResponse)) {
static::$latestResponse->transformNotSuccessfulException($e);
}
$property = new \ReflectionProperty($e, 'message');
$property->setAccessible(true);
$property->setValue($e, 'Failed.');
throw $e;
}
return $result;
}
ざっくり見ますと、最初の suppress 無名関数は、上記の3パターンに該当するかを判定する関数です。$_SERVER['argc'] や $_SERVER['argv'] などからコマンド情報を取得できますので、そんな感じでやっています。
で、この関数の実行結果が false なら、親クラスのメソッドを走らせて return します。
true の場合は、エラーメッセージを簡易版に置き換える処理を仕込んでいます。
この辺は、Laravel のソースをパクりつつ参考にして、書いています。
以上です。
Sail や Pest では試しておりませんので、必要に応じて調整して下さい🙇♂️
参考 URL
https://github.com/laravel/framework/pull/43943
https://github.com/laravel/framework/pull/44827
https://github.com/laravel/framework/pull/45416
雑感
これで、マウスをひたすら上までスクロールする回数も少なくなりそうです🍷
問題箇所等ありましたらコメント下さい。
Discussion