M1 macbook + SeleniumGridで並列WebUIテストの構築メモ

## 経緯

* WebUIのテストをやるぞ
* 画面ポチポチ検査するの面倒だな
* WebUIの自動テストをやろう
* Seleniumがあるじゃない
* まずはローカルで試そう
* Seleniumはdockerイメージがあるからそれを使おう
* docker for Desktopは好きじゃないからVM上のdockerでやろう
* VirtualBox使おう
* VirtualBoxはM1 macは非対応・・・だと・・・
* UTMというのがあるらしい。そっちを使おう
* UTM上にubuntu arm64版があったのでそれを使おう
* SeleniumのDocker imageを使おう
* arm64のUbuntu上では動かない・・・だと・・・
* arm64向けに作られたコンテナがあるから使おう
* pythonあまり使わないからNodejsでテスト書こう
* nodejsのテストはmochaテストフレームワークが使われているらしいから使おう
* assertionにはchaiを使うらしいから使おう

## 環境/ライブラリ

* M1 macbook BigSur(11.4)
* UTM  2.3.1 (38)
* Ubuntu20.04.3 server arm64 image
* docker 20.10.10
* docker-compose v2.0.1
* selenium docker image(arm64版)
	* https://hub.docker.com/u/seleniarm
* nvm 0.39.0
* npm 8.1.1
* nodejs v16.13.0
* mocha 9.1.3
* chai 4.3.4
* selenium-webdriver 4.0.0

## M1 mac上での仮想マシン構築

VirtualBoxを使用するつもりが、M1 macではそもそも動かないことがわかったので違う方法を模索した。

* VMWareを使う
	* まだ正式サポートでない
* Parallelsを使う
	* 一応正式サポートになっているが、持っていない
* UTMを使う
	* 動きそう

ということでUTMを使用することにする。

## UTMとUbuntu環境の準備

[Install Ubuntu ARM64 on Apple M1](https://github.com/utmapp/UTM/wiki/Install%20Ubuntu%20ARM64%20on%20Apple%20M1)
に書いてある通りに行った。Ubuntu Desktopはインストールしない。

## dockerとdocker-compose

それぞれインストールする。`apt-get`で特に問題なくインストールできる。細かいところは割愛する。

## Selenium Gridのdockerコンテナ起動

[公式](https://github.com/SeleniumHQ/docker-selenium)のdocker imageを使用し、Composeの[設定ファイル](https://github.com/SeleniumHQ/docker-selenium/blob/trunk/docker-compose-v2.yml)を参考にする。

```yml
# To execute this docker-compose yml file use `docker-compose -f docker-compose-v2.yml up`
# Add the `-d` flag at the end for detached execution
# To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v2.yml down`
version: '2'
services:
  chrome1:
    image: selenium/node-chrome:4.0.0-20211025
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
    ports:
      - "6900:5900"

  chrome2:
    image: selenium/node-edge:4.0.0-20211025
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
    ports:
      - "6901:5900"
      
  selenium-hub:
    image: selenium/hub:4.0.0-20211025
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"
```

とりあえず、2台のコンテナと1台のhubを用意し、nodeはどちらもchromeとする。適当なディレクトリを掘って、作成したファイルをdocker-compose.ymlにリネームする。

```zsh
$ docker-compose up -d
[+] Running 3/0
 ⠿ Container selenium-selenium-hub-1  Created                                                                                                           0.0s
 ⠿ Container selenium-chrome1-1       Created                                                                                                           0.0s
 ⠿ Container selenium-chrome2-1       Created                                                                                                           0.0s
Attaching to selenium-chrome1-1, selenium-chrome2-1, selenium-selenium-hub-1
selenium-selenium-hub-1  | standard_init_linux.go:228: exec user process caused: exec format error
selenium-selenium-hub-1 exited with code 1
selenium-chrome1-1       | standard_init_linux.go:228: exec user process caused: exec format error
selenium-chrome2-1       | standard_init_linux.go:228: exec user process caused: exec format error
selenium-chrome1-1 exited with code 1
selenium-chrome2-1 exited with code 1
```

エラーとなりコンテナが停止されてしまう。一旦すべてのコンテナをクリアし、単体で実行してみる。

```bash
$ docker container ls -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

$ docker run -d -p 4442-4444:4442-4444 --net grid --name selenium-hub selenium/hub:4.0.0-20211025
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
75d8094554124ebffdab995ee0ba3167affea3fc5d4ddee91730cf9c00177f78

$ docker container ls -a
CONTAINER ID   IMAGE                         COMMAND                  CREATED          STATUS                      PORTS     NAMES
75d809455412   selenium/hub:4.0.0-20211025   "/opt/bin/entry_poin…"   30 seconds ago   Exited (1) 29 seconds ago             selenium-hub
```

> WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested

どうやらarm64で動いているこのホストマシンとimageが合わないようだ。

## arm64向きdocker image

[https://github.com/SeleniumHQ/docker-selenium/issues/1076](https://github.com/SeleniumHQ/docker-selenium/issues/1076)

ここで議論されている中で、[arm64向きのimage](https://hub.docker.com/u/seleniarm)が公開されている。このcontainerは継続サポートはされないようだが、[ここ](https://github.com/seleniarm/docker-selenium)でarm64向きビルドを行っている人がいる。自前でもビルドできるので参考程度に。

docker-composeymlを修正して、以下とする。

```
# To execute this docker-compose yml file use `docker-compose -f docker-compose-v2.yml up`
# Add the `-d` flag at the end for detached execution
# To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v2.yml down`
version: '2'
services:
  chrome1:
    image: seleniarm/node-chromium:4.0.0-beta-1-20210215
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
    ports:
      - "6900:5900"

  chrome2:
    image: seleniarm/node-chromium:4.0.0-beta-1-20210215
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
    ports:
      - "6901:5900"

  selenium-hub:
    image: seleniarm/hub:4.0.0-beta-1-20210215
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"
```

imageの箇所を先程のものに差し替えただけ。前に失敗している場合は、`docker-compose down`で停止しておくこと。

```bash
$ docker-compose up -d
[+] Running 4/4
 ⠿ Network selenium_default           Created                                                                                                                                                                   0.0s
 ⠿ Container selenium-selenium-hub-1  Started                                                                                                                                                                   0.5s
 ⠿ Container selenium-chrome1-1       Started                                                                                                                                                                   1.4s
 ⠿ Container selenium-chrome2-1       Started     
 
 $ docker ps
CONTAINER ID   IMAGE                                           COMMAND                  CREATED          STATUS          PORTS                                                           NAMES
f2d71f110a11   seleniarm/node-chromium:4.0.0-beta-1-20210215   "/opt/bin/entry_poin…"   19 minutes ago   Up 19 minutes   0.0.0.0:6901->5900/tcp, :::6901->5900/tcp                       selenium-chrome2-1
48708d0256d9   seleniarm/node-chromium:4.0.0-beta-1-20210215   "/opt/bin/entry_poin…"   19 minutes ago   Up 19 minutes   0.0.0.0:6900->5900/tcp, :::6900->5900/tcp                       selenium-chrome1-1
bb0dfa0bcd4d   seleniarm/hub:4.0.0-beta-1-20210215             "/opt/bin/entry_poin…"   19 minutes ago   Up 19 minutes   0.0.0.0:4442-4444->4442-4444/tcp, :::4442-4444->4442-4444/tcp   selenium-selenium-hub-1
```

ここまで動いたら、hubが動作しているのか確認する。

UTM上で動いている仮想マシンのIPアドレスを確認し、実行PCからアクセスしてみる。今回の場合は`192.168.64.2:4444`にアクセスする。


![selenium-grid-browser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107986/bb775850-ef4f-2f3c-7d15-4177a5acb141.png)


2台のchromeが起動していることが確認できた。


## seleniumテスト

テストは、仮想マシンを動かしているPC上で実行する。nvm, npmで環境を準備し、以下のバージョンで行う。

* nvm 0.39.0
* Node.js : v16.13.0
* npm 8.1.1

適当にディレクトリを作成し、必要なライブラリをインストールする。

* elenium-webdriver
* mocha
* chai

```bash
$ mkdir node-selenium
$ cd node-selenium
% npm install --save-dev selenium-webdriver mocha chai
```
### テストの作成

```bash
$mkdir tests
```
testsディレクトリを作成し、2つのテストを作成する。内容は一緒だが並列にテストを行うために用意する。

```js
const { Builder, By, Key, Capabilities } = require("selenium-webdriver");
var chai = require('chai');
var assert = chai.assert;
let driver;

describe("Qiita表示テスト1", () => {

  before(() => {
    driver = new Builder()
    .usingServer("http://192.168.64.2:4444/wd/hub")
    .withCapabilities(Capabilities.chrome())
    .build();
  });

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

  it("Qiita表示1", async () => {
    await driver.get("https://qiita.com");
    const title = await driver.getTitle();
    assert.strictEqual(title, "Qiita");
  });

  it("Qiita検索遷移テスト1", async () => {
    await driver.get("https://qiita.com");
    await driver.findElement(By.xpath('/html/body/div[1]/div[1]/div/div/div/div[1]/form[1]/input')).sendKeys("selenium", Key.RETURN);
    const currentUrl = await driver.getCurrentUrl();
    assert.strictEqual(currentUrl, "https://qiita.com/search?q=selenium");
  });
});
```

>  .usingServer("http://192.168.64.2:4444/wd/hub")

この箇所については適宜自身の環境のhubに置き換えること。

test2.jsを作成し、describeとitの文字列を2に変更して、tests以下に保存。

```bash
ls tests
test1.js	test2.js
```

### テスト実行

package.jsonを編集して、scripts以下を追加する

```json
{
  "devDependencies": {
    "chai": "^4.3.4",
    "mocha": "^9.1.3",
    "selenium-webdriver": "^4.0.0"
  },
  "scripts": {
      "test": "mocha tests/  --timeout 20000"
  }
}
```

テストを実行する。

```bash
% ls
node_modules		package.json
package-lock.json	tests

$ npm test

> test
> mocha tests/  --timeout 20000

  Qiita表示テスト1
    ✔ Qiita表示1 (3652ms)
    ✔ Qiita検索遷移テスト1 (1965ms)

  Qiita表示テスト2
    ✔ Qiita表示2 (2564ms)
    ✔ Qiita検索遷移テスト2 (2632ms)


  4 passing (11s)
```

![selenium_serial.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107986/02d0d747-af82-cb51-745a-a56c2d2fdc5b.png)


これだけだと、並列実行されず一つずつテストされるためSelenium Gridの恩恵を受けることができない。

package.jsonを編集し`--parallel`をmocaのオプションに追加、再度実行する。


```json
"scripts": {
      "test": "mocha --parallel tests/  --timeout 20000"
  }
```

```bash
$ npm test

> test
> mocha --parallel tests/  --timeout 20000

  Qiita表示テスト1
    ✔ Qiita表示1 (3327ms)
    ✔ Qiita検索遷移テスト1 (3247ms)


  Qiita表示テスト2
    ✔ Qiita表示2 (4253ms)
    ✔ Qiita検索遷移テスト2 (3618ms)


  4 passing (8s)
```

![](selenium-grid-browser.png)![selenium_concurrent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107986/e204264d-3a9d-6b5e-1ca9-0a81ef707dad.png)

二つのノードで同時に実行され、Concurrencyは100%になり、誤差レベルでテスト時間が早くなった。

## まとめ

今回はテストが2つしかないため並列に実行する恩恵はほとんどないが、テスト数が増えれば増えるほど恩恵は大きくなる。
テストが増えた場合にはノードを増やすことでより一層テスト時間の短縮ができるだろう。

ちなみにM1 macじゃなければ普通に簡単なのでここまで面倒なことをはしなくて良い。さっさとIntel macでやればよかった。