AngularのテストをDocker(CentOS 7)上で回すときに設定したこと

Angular CLIで作成したプロジェクトは初期設定でテストフレームワークである[Jasmine](https://jasmine.github.io/)とテストランナーである[Karma](https://karma-runner.github.io/)を備えています。デフォルト状態でテスト（`ng test`）を行うと、Karmaによってブラウザが起動し、Jasmineで書かれたテストコードが実行されます。このとき、`karma.conf.js`のconfigに記載されたブラウザが起動する仕組みになっています。開発PC等でテストを実行するときconfigに記載されたブラウザが予めインストールされていれば問題なくテストを実行できます。
しかし、CIサーバやDockerなどの通常ブラウザがインストールされていない環境でテスト実行する場合は別途ブラウザをインストールする必要があります。
この記事ではDockerでAngularのテストを実行するために設定したことを記載していきます。また、公式ドキュメントの内容だけでは設定がうまくいかず若干ハマったポイントもあるのでそこについても触れたいと思います。

# 前提
## 今回使用するフレームワーク等のバージョン
以下バージョンを前提としています。バージョン次第で動作が変わる可能性があります。

|フレームワーク・ツール|バージョン|
|:-:|:-:|
|Angular|9.1.9|
|Karma|5.0.9|
|Jasmine|2.8.0|
|Puppeteer|3.3.0|

また、今回使用するDocker上のOSはCentOS 7を使用します。

## Dockerfile
テスト実行のための環境構築はdocker buildで行い、テスト実行はdocker runで行うものとします。
またAngularテストの設定前に、最低限node.jsのインストールやnpm install、テスト実行コマンドは用意しているものとします。

```dockerfile:Dockerfile
FROM centos:centos7

RUN curl -sL https://rpm.nodesource.com/setup_14.x | bash - && yum install -y nodejs
WORKDIR /app
COPY package*.json /app/
RUN npm i
COPY . /app/
CMD ["npx", "ng", "test", "--no-watch"]
```

```:.dockerignore
node_modules
```

ちなみにAngularはデフォルトでウォッチモードでテストを実行します。ウォッチモードではKarmaがデーモンのように動いており、Angularのコードを変更するとそれを検知して自動でテストが再実行されます。
ただ今回のようにDocker上で動かす場合はウォッチモードで動かす必要はなく、プロセスを停止させる必要もあるため、`ng test`に`--no-watch`オプションを付与することでウォッチモードなしにしています。

## 今回使用したサンプルコード
今回のサンプルコードは以下GitHubに上げています。

- [kawakawaryuryu/sample-angular-test: sample angular test on docker](https://github.com/kawakawaryuryu/sample-angular-test)

# 設定したこと
基本的にはAngularの公式ドキュメントに記載があるのでそれに従って設定していきます。

- [Angular 日本語ドキュメンテーション - テスト#chromeでのciテスト用にcliを設定する](https://angular.jp/guide/testing#chrome%E3%81%A7%E3%81%AEci%E3%83%86%E3%82%B9%E3%83%88%E7%94%A8%E3%81%ABcli%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)

ただし、2と3については公式ドキュメントには記載されていません。調査していく中で分かったことになります。

## 1. ヘッドレスブラウザの設定
通常Docker上でのテストの実行にGUIブラウザは不要のため、ヘッドレスブラウザを使用します。今回はChromeを利用します。
公式ドキュメントに記載されているように`karma.conf.js`のconfigの`broswers`プロパティと`customLaunchers`プロパティに以下のように設定します。

```js:karma.conf.js
module.exports = function (config) {
  config.set({
    ...
    browsers: ['Chrome'],
    customLaunchers: {
      ChromeHeadlessCI: {
        base: 'ChromeHeadless',
        flags: ['--no-sandbox']
      }
    },
    ...
  });
};
```

テストを実行する際は上記の設定をオプションとして指定します。

```diff:Dockerfile
COPY . /app/
-CMD ["npx", "ng", "test", "--no-watch"]
+CMD ["npx", "ng", "test", "--no-watch", "--browsers=ChromeHeadlessCI"]
```

Dockerfileをビルドしてテスト実行してみます。

```
# ビルド
$ docker build -t sample-angular-test .
...

# テスト実行
$ docker run sample-angular-test
...
14 06 2020 10:15:58.255:WARN [karma]: No captured browser, open http://localhost:9876/
14 06 2020 10:15:58.265:INFO [karma-server]: Karma v5.0.9 server started at http://0.0.0.0:9876/
14 06 2020 10:15:58.266:INFO [launcher]: Launching browsers ChromeHeadlessCI with concurrency unlimited
14 06 2020 10:15:58.275:INFO [launcher]: Starting browser ChromeHeadless
14 06 2020 10:15:58.277:ERROR [launcher]: No binary for ChromeHeadless browser on your platform.
  Please, set "CHROME_BIN" env variable.
14 06 2020 10:16:09.712:WARN [karma]: No captured browser, open http://localhost:9876/
```

エラーが出てテストが失敗します。
エラー内容を見るとChromeヘッドレスブラウザのバイナリがない、CHROME_BIN環境変数を設定しろと言っています。開発PC等で動かすときは通常Chromeのバイナリがすでにインストールされていることが多いためこのエラーに出くわすことは少ないですが、DockerなどではChromeバイナリがインストールされていないため別途必要になります。

ということで次にChromeバイナリをインストールしていきます。

## 2. Chromeバイナリのインストール
ヘッドレスChromeを動かすためにChromeのバイナリをインストールします。
[karma-chrome-launcher](https://github.com/karma-runner/karma-chrome-launcher)プラグインのREADMEにも記載されているように、今回はPuppeteerに封入されているChromeバイナリを利用しようと思います。

- [Headless Chromium with Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer)

必要なパッケージをインストールします。READMEにはkarma-chrome-launcherのインストールも書いてありますが、Angular CLIで作成したプロジェクトにはデフォルトで入っているのでPuppeteerのみインストールします。
このインストールに限ってはローカルPCで行っています。（本来はpackage.jsonに追加すればよいが面倒なため`npm install`で追加している）

```
$ npm i -D puppeteer
```

インストール後、Chromeバイナリのパスを示すCHROME_BIN環境変数を設定します。

```js:karma.conf.js
process.env.CHROME_BIN = require('puppeteer').executablePath(); // 新たに追加

module.exports = function (config) {
  config.set({
    ...
  });
};
```

Chromeバイナリのインストールと環境変数の設定ができました。
この状態でもう一度Dockerfileのビルド、テスト実行をしてみます。

```
# ビルド
$ docker build -t sample-angular-test .
...

# テスト実行
$ docker run sample-angular-test
...
14 06 2020 10:46:59.325:WARN [karma]: No captured browser, open http://localhost:9876/
14 06 2020 10:46:59.344:INFO [karma-server]: Karma v5.0.9 server started at http://0.0.0.0:9876/
14 06 2020 10:46:59.345:INFO [launcher]: Launching browsers ChromeHeadlessCI with concurrency unlimited
14 06 2020 10:46:59.354:INFO [launcher]: Starting browser ChromeHeadless
14 06 2020 10:46:59.438:ERROR [launcher]: Cannot start ChromeHeadless
	/app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory

14 06 2020 10:46:59.439:ERROR [launcher]: ChromeHeadless stdout:
14 06 2020 10:46:59.439:ERROR [launcher]: ChromeHeadless stderr: /app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory

14 06 2020 10:46:59.452:INFO [launcher]: Trying to start ChromeHeadless again (1/2).
14 06 2020 10:46:59.569:ERROR [launcher]: Cannot start ChromeHeadless
	/app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory

14 06 2020 10:46:59.572:ERROR [launcher]: ChromeHeadless stdout:
14 06 2020 10:46:59.573:ERROR [launcher]: ChromeHeadless stderr: /app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory

14 06 2020 10:46:59.622:INFO [launcher]: Trying to start ChromeHeadless again (2/2).
14 06 2020 10:47:00.333:ERROR [launcher]: Cannot start ChromeHeadless
	/app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory

14 06 2020 10:47:00.335:ERROR [launcher]: ChromeHeadless stdout:
14 06 2020 10:47:00.336:ERROR [launcher]: ChromeHeadless stderr: /app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory

14 06 2020 10:47:00.455:ERROR [launcher]: ChromeHeadless failed 2 times (cannot start). Giving up.
14 06 2020 10:47:07.813:WARN [karma]: No captured browser, open http://localhost:9876/
```

またエラーが出ます。Chromeヘッドレスブラウザがスタートできないと言っています。原因はPuppeteerに封入されたChromeバイナリが必要とする共有ライブラリ（libX11.so.6）が存在しないためのようです。

ということで次にPuppeteerのChromeバイナリが必要なパッケージをインストールしていきます。

## 3. PuppeteerのChromeバイナリが必要なパッケージのインストール
Linux上でPuppeteerのヘッドレスChromeを利用する場合はいくつか依存パッケージが必要になります。OSによって必要なパッケージは異なります。
PuppeteerのトラブルシューティングページにOSごとに必要なパッケージの記載があるので詳しくはこちらを見てください。

- [puppeteer/troubleshooting.md at master · puppeteer/puppeteer](https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md)

今回はCentOS 7の環境で進めたため、CentOS 7に必要なパッケージを追加します。

```diff:Dockerfile
FROM centos:centos7
-
+
+RUN yum -y install alsa-lib.x86_64 atk.x86_64 cups-libs.x86_64 GConf2.x86_64 gtk3.x86_64 ipa-gothic-fonts libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXrandr.x86_64 libXScrnSaver.x86_64 libXtst.x86_64 pango.x86_64 xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-fonts-cyrillic xorg-x11-fonts-misc xorg-x11-fonts-Type1 xorg-x11-utils && yum update nss -y
RUN curl -sL https://rpm.nodesource.com/setup_14.x | bash - && yum install -y nodejs
```

最後にビルド＆テスト実行します。

```
# ビルド
$ docker build -t sample-angular-test .
...

# テスト実行
$ docker run sample-angular-test
...
14 06 2020 11:57:40.881:INFO [karma-server]: Karma v5.0.9 server started at http://0.0.0.0:9876/
14 06 2020 11:57:40.899:INFO [launcher]: Launching browsers ChromeHeadlessCI with concurrency unlimited
14 06 2020 11:57:40.909:INFO [launcher]: Starting browser ChromeHeadless
14 06 2020 11:57:49.489:INFO [Chrome Headless 83.0.4103.0 (Linux x86_64)]: Connected on socket 3oZV9oh1wxO8T1UOAAAA with id 83185974
Chrome Headless 83.0.4103.0 (Linux x86_64): Executed 3 of 3 SUCCESS (0.481 secs / 0.319 secs)
TOTAL: 3 SUCCESS
TOTAL: 3 SUCCESS
```

以上で問題なくDocker（CentOS 7）上でもAngularテストを実行することができました。