Docker上でnext.jsとseleniumをワンライナーで起動からブラウザ自動テストまでをしたい

## はじめに
岩手県立大学とか、岩手の人たち Advent Calendar 2022の14日目の記事です。


[@gazeuse1442](https://twitter.com/gazeuse1442)と申します。
情報系の専門学生でセキュリティやバックエンドなど**広く浅く**学習しています。
今回、岩手県立大学には在籍していませんが、岩手の某専門学校に在籍しているため参加させていただきました。

𝑰𝑾𝑨𝑻𝑬 𝑩𝑰𝑮 𝑳𝑶𝑽𝑬______

## 記事について
DockerでNextjs+selenumの環境を構築した時の備忘録です。
この記事ではNextJS + selenium + mochaを使ったDocker Composeでのテスト環境の構築・シェルスクリプトでの実行を解説します。

[サンプルコード](https://github.com/hikaru0112/next_selemium)
## 本題

まずは今回組むDockerのコンテナ構成について、
実行するテストのWebページについては`npx create-next-app@latest`で生成されるnextJSのデフォルトページを使用します。


使用するコンテナは以下の通り

* `next-app`
    * NextJSを動作させるコンテナ
    * node-slimを使用します
* `selenium-hub`
    * 複数のseleniumを管理できる便利なコンテナ
* `selenium-node`
    * node(chromium)をテスト環境にして実行するコンテナ
* `selenium-test-runner`
    * テストを実行させるコンテナ
    * node-slimを使用します

next-appで生成・取得できるデータをテストに回します

大まかなフォルダ構成について
```
.
├── docker-compose.yml
├── selenium_runner　　　
│   　#testを動作させるコンテナ
└── selenium_test_next　　　　
   　 #nextjsを動作させるコンテナ
```

### Docker-compose について
**M1macなどのARMプロセッサの場合、selenium-hubがうまく起動しない場合がある**ので`seleniarm-node`や`seleniarm/hub`を代わりに指定します。depends_onを指定しないと他の依存コンテナより早く起動してしまうため、そのコンテナの名前を指定しないといけません。next-appとselenium-test-runnerについては、パッケージのインストールなど別途Dockerfileに記述しますが今回は省略。


```yml:docker-compose.yml
version: "3.9"
services:
  next-app:
    build: ./selenium_test_next
    ports:
      - "8080:3000"
    volumes:
      - ./selenium_test_next:/app
      - next-node-modules:/app/node_modules
    networks:
      - selenium-grid

  selenium-hub:
    image: selenium/hub
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"
    environment:
      GRID_BROWSER_TIMEOUT: 10000
      GRID_NEW_SESSION_WAIT_TIMEOUT: 20000
      GRID_NODE_POLLING: 300
      GRID_TIMEOUT: 10000
    networks:
      - selenium-grid
    depends_on:
      - next-app

  selenium-node:
    image: selenium/node-chromium:latest
    shm_size: 2g
    depends_on:
      - selenium-hub
    ports:
      - "7901:7900"
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_EVENT_BUS_PUBLISH_PORT: 4442
      SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
    networks:
      - selenium-grid

  selenium-test-runner:
    tty: true
    build: ./selenium_runner
    volumes:
      - ./selenium_runner:/app
      - test-runner-node-modules:/app/node_modules
    depends_on:
      - selenium-node
    networks:
      - selenium-grid


networks:
  selenium-grid:
    driver: bridge
volumes:
  next-node-modules:
  test-runner-node-modules:
```

### selenium-test-runnerについて
テスト環境には、
* selenium-webdriver
* mocha
* chai
を使用します。

今回はタイトルとh1タグのテキストが指定されたものかをテストします。
2つ目のテストは失敗するようにしています。



```typescript:index.ts
import {
  Builder,
  WebDriver
} from 'selenium-webdriver';

import { assert } from 'chai';


const TIMEOUT_MILLISEC = 10000;

let driver: WebDriver;

describe("サンプルページ", () => {
  before(() => {
    driver = new Builder()
      .forBrowser("chrome")
      .usingServer("http://selenium-hub:4444/wd/hub")
      .build();
  });

  after(() => {
    return driver.quit();
  });

  it("タイトルを検証する", async () => {
    await driver.get("http://next-app:3000");
    const title = await driver.getTitle();
    assert.strictEqual(title, "Create Next App");
  });

  it("h1タグの中身を検証する", async () => {
    await driver.get("http://next-app:3000");
    const h1 = await driver.findElement({ tagName: 'h1' });
    const h1_text = await h1.getAttribute("innerText")
    assert.strictEqual(h1_text, "Create Next App");
  });
});

```

テストを実行したいWebサーバーのURLやポートを適宜変更してください。

```json:package.json
"scripts": {
    "test": "mocha index.mjs --timeout 10000"
}
```

### 実行について
以下のコマンドで実行。
```shell:shell
$ docker compose up 
```
以下のテスト実行結果を取得することができます。

```log
 > test
 > mocha index.mjs --timeout 10000
 
 
 
   サンプルページ
     ✔ タイトルを検証する (1087ms)
     1) h1タグの中身を検証する
 
 
   1 passing (2s)
   1 failing
 
   1) サンプルページ
        h1タグの中身を検証する:
 
       AssertionError: expected 'Welcome to Next.js!' to equal 'Create Next App'
       + expected - actual
 
       -Welcome to Next.js!
       +Create Next App
       
       at Context.<anonymous> (file:///app/index.mjs:38:12)
       at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
```


`selenium-test-runner`以外のコンテナが実行されていれば、問題なくテストを行うことができますが、ビルドから実行を行った場合NextJSやselenium-hubの起動が遅く、`selenium-test-runnerで実行するテスト`が先に実行されてしまうため、エラーが発生することがあります。


そこで、シェルスクリプトを使ってWebサーバーが起動を完了してからテストを実行できるように設定していきます。


### シェルスクリプトの作成
Webサーバーが起動したかどうかを判断するために、今回は`curl`を使用しました。
テストの実行後、テストの標準出力を`./selenium_log/node{yymmddhhmm}.log`に保存できるようにします。

```bash:test.sh
#!/usr/bin/env bash
set -e

if [ -z "$(docker container ls -q -f name='next-app')" ]; then
   echo "container build"
    nohup docker compose -f docker-compose.yml up next-app selenium-hub selenium-node  &>/dev/null & 
    while :; do
        #nextが起動するまで待機する
        if [ "$(curl -fs localhost:8080 | grep '<html>')" ] &&
         [ "$(curl -fs http://localhost:4444/ui | grep '<!doctype html>')"  ]; then
            echo "next-app container build complete."
            break # 起動したら抜ける
        fi
        sleep 1
    done
fi

echo "start selenium test."
  if [ ! -e ./selenium_log ]; then
    # 存在しない場合
    mkdir ./selenium_log
  fi

  #いい感じにlogファイルに保存させる
docker compose up selenium-test-runner | (
    v=$(cat)
    printf "$v"
    echo "$v" | grep --line-buffered 'selenium-test-runner' |
      awk '/> test/,/exited with code/' | 
      cut -f 2 -d "|" |
      sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g" > ./selenium_log/node-$(date +%Y%m%d%H%M).log)

```


一部不要なログが保持されてしまいますが、これでワンライナーでコンテナの起動からテストのログを保持することができます。

### さいごに(余談)

プロフィールのkotlinを1年半くらいタイポしてました。
一目惚れしたのに名前を間違えるなんて最低...
![Screenshot 2022-12-13 at 11.55.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607985/c710a6f4-32d5-7b9a-bd37-350fb21555c3.png)