🤏

Laravel 自動テスト時のエラーメッセージを抑制する

2023/02/17に公開

前書き

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}
     2526▕     /**

  ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   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}
     2526▕     /**

  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パターンのコマンドでテストを走らせた時(=テスト全体を走らせる時)です。

  1. vendor/bin/phpunit(オプション無し)
  2. php artisan test(オプション無し)
  3. 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