Laravel Duskで進行状況を出力しながらテストする

# 背景
Laravelプロジェクトを触った事がある者には、ブラウザを操作してテストするLaravel Dusk（以降、Dusk）という自動テストツールの存在を聞いた事があると思います。

このDuskを既に実践していたりこれから利用を考えているような方に向けて情報を発信します。

Duskは実行上Headless Chromeを使用してブラウザテストを行うため実行環境に合った専用ChromeDriverも必要になり、今回はこの自動管理されているChromeDriverのバージョン更新に伴う挙動の変化について記述します。

# 事象： 一括実行テストを行う時に進行状況がわかりにくい

この事象はChromeDriverのバージョンを 128以降 に変更すると現れます。
127以前では、テストが実行されている間はDusk内部で関連するサブモジュール群が動作する事でログ出力（INFO、ERRORレベルなど）が流れるので状況は一先ず分かりましたが、
127より上のバージョンに更新すると、上記ログ出力が致命に近いレベル（ERRORやWARNING）に制限されるのか実行中に流れるログが大きく減ります。
見た目では、正常に動いているのか異常が起きて待機中になっているのか区別が難しく感じます。

## 検証環境
検証した環境はWindows機で、MacOSは実施しておりません。
- Windows 10 Pro(64bit)
- PHP 8.2.17
- Laravel 9.42
- Laravel Dusk 8.2


## 実行時間の比較
下記に、適当なテストファイル（サンプル）とその実行時間の差を貼ります。

### ある書籍まとめ紹介ページに表示の全本の名前や購入先URLを確認する事を前提
```BookCheckTest.php
<?php

namespace Tests\Browser;

use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class BookCheckTest extends DuskTestCase
{
    /**
     * Create the RemoteWebDriver instance.
     */
    protected function driver()
    {

        $options = (new ChromeOptions)->addArguments([
            '--disable-gpu',
            '--headless',
            '--window-size=1920,1080',
            '--no-sandbox',
            '--ignore-certificate-errors',
            '--ignore-ssl-errors'
        ]);

        return RemoteWebDriver::create(
            $_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515',
            DesiredCapabilities::chrome()->setCapability(
                ChromeOptions::CAPABILITY,
                $options
            )
        );
    }


    /**
     * Test1：　check all Book links
     * text content not empty.
     * 
     * @group test1
     */
    public function testBookLinks()
    {
        $this->browse(function (Browser $browser) {
            $target = 'https://www.lifehacker.jp/article/2412-recommended-book-for-winter-holiday/';
            $browser->visit($target)
                ->waitFor('.footer_lFooter__6YP2a') // フッター部まで読み込みを待つ
                ->assertPresent('div.article_pArticle_Embed__T5UsK.embedblock') // div要素が存在するか確認
                ;

            // 全ての div.article_pArticle_Embed__T5UsK.embedblock を取得し、内部の a.araklet-title-link を確認
            $links = $browser->elements('div.article_pArticle_Embed__T5UsK.embedblock a.araklet-title-link');

            $this->assertNotEmpty($links, 'リンクが見つかりませんでした');

            foreach ($links as $link) {
                $text = $link->getText();
                $this->assertNotEmpty($text, 'リンクのテキストが空です');
            }
        });
    }

    /**
     * Test2：　check all Book links
     * text content not empty.
     * 
     * @group test2
     */
    public function testBookLinks2()
    {
        $this->browse(function (Browser $browser) {
            $target = 'https://www.lifehacker.jp/article/2412-recommended-book-for-winter-holiday/';
            $browser->visit($target)
                ->waitFor('.footer_lFooter__6YP2a') // フッター部まで読み込みを待つ
                ->assertPresent('div.article_pArticle_Embed__T5UsK.embedblock') // div要素が存在するか確認
                ;

            // 全ての div.article_pArticle_Embed__T5UsK.embedblock を取得し、内部の a.araklet-title-link を確認
            $links = $browser->elements('div.article_pArticle_Embed__T5UsK.embedblock a.araklet-title-link');

            $this->assertNotEmpty($links, 'リンクが見つかりませんでした');

            foreach ($links as $link) {
                $text = $link->getText();
                $this->assertNotEmpty($text, 'リンクのテキストが空です');
            }
        });
    }

    /**
     * Test2：　check all Book links
     * text content not empty.
     * 
     * @group test3
     */
    public function testBookLinks3()
    {
        $this->browse(function (Browser $browser) {
            $target = 'https://www.lifehacker.jp/article/2412-recommended-book-for-winter-holiday/';
            $browser->visit($target)
                ->waitFor('.footer_lFooter__6YP2a') // フッター部まで読み込みを待つ
                ->assertPresent('div.article_pArticle_Embed__T5UsK.embedblock') // div要素が存在するか確認
                ;

            // 全ての div.article_pArticle_Embed__T5UsK.embedblock を取得し、内部の a.araklet-title-link を確認
            $links = $browser->elements('div.article_pArticle_Embed__T5UsK.embedblock a.araklet-title-link');

            $this->assertNotEmpty($links, 'リンクが見つかりませんでした');

            foreach ($links as $link) {
                $text = $link->getText();
                $this->assertNotEmpty($text, 'リンクのテキストが空です');
            }
        });
    }
}
```

### 実行結果

``` ChromeDriver バージョン127.txt
php artisan dusk tests/Browser/BookCheckTest.php

Warning: TTY mode is not supported on Windows platform.
PHPUnit 9.6.13 by Sebastian Bergmann and contributors.


DevTools listening on ws://127.0.0.1:53851/devtools/browser/156c18a7-62be-4960-9f75-2b82e2e129ae
[0205/234221.933:ERROR:ssl_client_socket_impl.cc(883)] handshake failed; returned -1, SSL error code 1, net_error -101
[0205/234222.734:INFO:CONSOLE(1)] "Visibility threshold pct=50, Visibility threshold ms = 0", source: https://tag.durationmedia.net/sites/11516/dm.js (1)
[0205/234222.991:INFO:CONSOLE(6)] "%cExcuted AdScript! color: #008000", source: https://www.lifehacker.jp/_next/static/chunks/pages/_app-ebd38f51dd7bc899.js (6)
...
　※百行近くあるが意味はないので途中省略
...
[0205/234258.182:INFO:CONSOLE(1)] "smartocto's Tentacles 2.4.5.1 loaded...", source: https://tentacles.smartocto.com/ten/tentacle.js (1)
[0205/234258.282:INFO:CONSOLE(1)] "Tentacles is initiating Article Page", source: https://tentacles.smartocto.com/ten/tentacle.js (1)
.                                                                 3 / 3 (100%)

Time: 00:42.835, Memory: 20.00 MB

OK (3 tests, 69 assertions)
```

```ChromeDriver バージョン128以降.txt
php artisan dusk tests/Browser/BookCheckTest.php

Warning: TTY mode is not supported on Windows platform.
PHPUnit 9.6.13 by Sebastian Bergmann and contributors.

[13584:5672:0205/104513.784:ERROR:ssl_client_socket_impl.cc(876)] handshake failed; returned -1, SSL error code 1, net_error -101
...                                                                 3 / 3 (100%)

Time: 00:36.744, Memory: 22.00 MB

OK (3 tests, 69 assertions)
```

# 対処法： テスト実行コマンドにオプション「--debug」を付与
コマンド
```bash
php artisan dusk tests/Browser/BookCheckTest.php --debug
```
出力結果
```bash
Warning: TTY mode is not supported on Windows platform.
PHPUnit 9.6.13 by Sebastian Bergmann and contributors.

Test 'Tests\Browser\BookCheckTest::testBookLinks' started
Test 'Tests\Browser\BookCheckTest::testBookLinks' ended
Test 'Tests\Browser\BookCheckTest::testBookLinks2' started
Test 'Tests\Browser\BookCheckTest::testBookLinks2' ended
Test 'Tests\Browser\BookCheckTest::testBookLinks3' started
Test 'Tests\Browser\BookCheckTest::testBookLinks3' ended

Time: 00:40.181, Memory: 22.00 MB

OK (3 tests, 69 assertions)
```

## 何が良くなるの？
 - 一括実行時の進行具合確認が楽になる（テスト関数の```@group名```で開始終了宣言が出力）
 - テスト内容自体に問題があったり何らかのエラーが生じてテストが進んでいない場合の兆候・問題を発見する上でも助かる

# その他の便利サイト
- 指定バージョンのChromeドライバのインストールについて
  [公式ドキュメント](https://readouble.com/laravel/9.x/ja/dusk.html)の目次「Chromeドライバインストールの管理」でも説明されていますが、下記のようにバージョン指定したdusk:installコマンドにより、デフォルトChromeドライバ（その時点での最新版）以外を準備できます。
```
php artisan dusk:chrome-driver 127
```

- 古いChrome本体やドライバの入手先
Chrome for Testing：　https://googlechromelabs.github.io/chrome-for-testing/#stable
Chrome ブラウザ（Stable 版またはベータ版）：　https://chromeenterprise.google/intl/ja_jp/download/?modal-id=download-chrome#windows-download
過去版のChrome ブラウザ（※旧版ソフトウェアサイト）：　https://google-chrome.en.uptodown.com/windows/versions