Chrome for Testing で自動テストを手元で簡単に動かせるようにした

# はじめに

こんにちは、西薗（ [@yurizono](https://twitter.com/yurizono) ）です。一人目QAとしてニューズピックスに入社してから3年弱、ひたすらテスト設計やバグ管理、仕様整理、ときには自分でテストもしつつ、ツールも作りつつやってきました。最近は自動テストにも着手し始めています。

今日は小ネタとして、 Chrome for Testing を初めて真面目に使った話をします。

# Chrome for Testing とは

詳しくは[本家](https://developer.chrome.com/blog/chrome-for-testing)をご覧ください、なのですが、ざっくりいうと以下の課題を解決するために作られた Chrome の亜種です。

## バージョンの問題

Chrome で自動テストを動かすためには ChromeDriver という外部ツールが必要になります。これは Chrome のバージョンに合わせたものを使う必要があるのですが、そもそも Chrome は自動アップデートされるのでバージョンの固定ができません。従って、昨日は動いていた自動テストが今日は動かない、ということがままありました。

当然、 Chrome for Testing 以前も、これには色々と対策が行われてきました。

## 課題に対する、これまでの対策

固定のバージョンの Chrome と ChromeDriver が一緒にインストールされている Docker Image を使っていた方も多いと思います。弊社もその仕組みでリリース時のテストが動いています。

ただ、 Chrome のバージョンをあげようとすると Docker Image の更新版が出ていなければ上げられませんし、最近だと Apple Sillicon の MacOS だとそのイメージが動かない、なんてこともありました。

また意外と辛いのが、 headless mode 周り。 CI に組み込む分には裏で勝手に動いてくれて良いのですが、手元で開発するときはブラウザが実際に操作される様子を見たいですよね。しかし動作環境がコンテナだと、そこに頑張って GUI セッションを張って手元の端末で画面を見る、みたいな本質的でないところに工夫が必要だったりします。

極め付けが通信内容の確認で、 Proxyman などを使ってブラウザの通信内容を見ながら意図したヘッダーが付与されているかなどを確認したいのですが、これをコンテナでやろうとすると証明書をコンテナイメージのどこかに仕込みにいく必要があったりで、なかなかにハードルが上がります。

結論、ローカルで、普通にブラウザが立ち上がってテストが動けばそれが一番楽なのでそうしたい。が、先述のバージョン固定が難しいという問題があって、それを Chrome for Testing が解決してくれた、という話です。

## 改めて Chrome for Testing とは
ざっくり、私の理解はこんな感じです。
- 任意のバージョンのブラウザを、対応するドライバと一緒に取得できる。
- 複数のバージョンのブラウザとドライバを共存させられる。
- 厳密には Chrome ではないが、限りなく Chrome に近いので、自動テストなどはこれを使うことを Google が推奨している。

<!-- ちなみに Chromium を使えば？という話もありますが、こちらは Chrome のソースコードを元に作られてはいるものの、公開時点からは完全に枝分かれしている状態ですから、似てはいるけど別のもの、と割り切って使いましょう。-->

# 実際に Chrome for Testing を使ってみた

今回、私は **テストをローカルで動かすために** Chrome for Testing を使いました。開発者はたくさんいますので、みんなが手元で、できる限り簡単にテストを動かせるようにしたい、が要件です。ちなみに環境としては、テストフレームワークとして WebDriverIO を使っていて、言語は TypeScript です。 CI 用とは別に、ローカル実行用の設定ファイル（ここでは `wdio.local.conf.ts` とします）を用意しています。

とりあえずインストールコマンドが必要だろう、ということで `package.json` にこんなコマンドを用意しました。

```json:package.json
"scripts": {
  "install-chrome": "npx @puppeteer/browsers install chrome@119.0.6045.105"
  "install-driver": "npx @puppeteer/browsers install chromedriver@119.0.6045.105"
}
```

そして、テストの設定ファイル側では、上記でインストールした Chrome と ChromeDriver を使うように設定を書いておきます。

```tsx:wdio.local.conf.ts
export const config: Options.Testrunner = {
  capabilities: [
    {
      browserName: "chrome",
      "goog:chromeOptions": {
        binary: "chrome/mac_arm-119.0.6045.105/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
        args: [ /* 省略 */ ],
      },
    },
  ],
  services: [
    [
      "chromedriver",
      { chromedriverCustomPath: "chromedriver/mac_arm-119.0.6045.105/chromedriver-mac-arm64/chromedriver" },
    ],
  ],
};
```

さて、上記でバッチリ！と思ったのですが、問題が2つありました。

1. Chrome のバージョンを新しくする場合、両方のファイルを修正しなければならない。
2. Intel Mac だと動かない（インストールパスが異なる）

一つ目は気づいていながら目を瞑っていたのですが、二つ目は言われて気づきました。確かにパスに `arm64` とかって書いてありました。

# どうするか

Chrome for Testing をインストールするのには `@puppeteer/browsers install` というコマンドを使います。この `install` 以外に、どうせインストールした先のパスを取得するコマンドがあるんだろう？と思って調べたらありませんでした。ないんだ……。

README など読んでみると、公開 API を使って何とかしろと書いてありましたので、そうします。

# こうなりました

バージョンは別のファイルに書いておきます。

```text:env/cft.env
CHROME_VERION=119.0.6045.105
```

これを読んでくれば良いですね。

```json:package.json
"scripts": {
  "install-chrome": "source env/cft.env; npx @puppeteer/browsers install chrome@${CHROME_VERION}"
  "install-driver": "source env/cft.env; npx @puppeteer/browsers install chromedriver@${CHROME_VERION}"
}
```

TypeScriptの方は `dotenv` を使ってこんな感じに。

```tsx:wdio.local.conf.ts
configDotenv({ path: "env/cft.env" }); // 環境変数をファイルから読み込む

export const config: Options.Testrunner = {
  capabilities: [
    {
      browserName: "chrome",
      "goog:chromeOptions": {
        binary: computeExecutablePath({ // インストール先のパスを取得する
          browser: Browser.CHROME,
          buildId: process.env.CHROME_VERSION ?? "",
          cacheDir: "./",
          platform: detectBrowserPlatform(), // Intelか否かも固定せずその場で取得
        }),
        args: [ /* 省略 */ ],
      },
    },
  ],
  services: [
    [
      "chromedriver",
      { 
        chromedriverCustomPath: computeExecutablePath({ // インストール先のパスを取得する
          browser: Browser.CHROMEDRIVER,
          buildId: process.env.CHROME_FOR_TESTING ?? "",
          cacheDir: "./",
          platform: detectBrowserPlatform(), // Intelか否かも固定せずその場で取得
        }),
      },
    ],
  ],
};
```
以上、朝の時間を使ってさくっと実装できました。できましたが、 PR を投げたら型チェックでエラーを吐いていたので、それを解決したりする方に時間がかかりました。 TypeScript の型、難しくないですか。

# おわりに
Chrome for Testing を使うようにしたことで、バージョンを上げるのも簡単、手元で headed mode でテストを動かすのも簡単、通信内容の確認も簡単、と良いことだらけでした。まだ使ってない、という方はぜひ。