Cypressのガチ運用作戦

# Cypressのガチ運用作戦
この記事は [Selenium/Appium Advent Calendar](https://qiita.com/advent-calendar/2019/selenium_and_appium) 2019の20日目の記事です。


> _この記事は映画観すぎて現実と映画の境にさまよったエンジニアの記録なので一部妄想が含まれている_

Cypressを日々の任務に組み込んでみて得た苦しみ、痛み、ストレス、達成感などを書いていきます。
Cypressの導入や使い方は[以前の記事](https://qiita.com/sinpaout/items/4313ed75e603508696b1)を参照してください。

> 最近また観た[プライベート・ライアン](https://eiga.com/movie/48770/) も最高の作品だ
![Saving private ryan](https://m.media-amazon.com/images/M/MV5BMTM5MzI1MDE5Nl5BMl5BanBnXkFtZTcwNDM3NzgwMw@@._V1_SY1000_CR0,0,1493,1000_AL_.jpg)
> また観るきっかけになったのは `あの映画の銃声って第二次世界大戦本物の銃声だったよ` って自慢げに話してきた弟だった。やつはメイキング映像が大好き。

## 隊の編制

我らの隊は30人規模の中隊でこの国を統一し平和をもたらすという志しで結成されている。隊を率いる中尉は様々な苦難を乗り越えてきた経験豊かな方だった。

> _14ヶ月前から関わっているプロジェクトはHR業界をリードし働く人が幸せになれる社会を目指す目的で結成されているチームだった。_

## 忘れ去られた
約3年間任務を遂行のためにひたすら前へ進み、様々な問題を対象してきた隊員たちは疲弊していた。それに気づいた中尉が任務遂行を止め隊の編制見直し、解決し改善して行くように試みた。Cypress検査もその一環だった。Cypress検査の仕組みを構築したがこれも8ヶ月前の話しで現在はCypressの検査はされなくなって忘れ去られた。
※ Cypress検査とは健康診断のようなもので隊員と隊の状態をチェックし異常な状態を検知できるしくみである。

> _IT業界でも新機能追加を止めて改善期間を設け、テストなどを導入し、開発サイクルのスピードを上げる試みをすることはよくある。しかしテストがメンテナンスされなくなって忘れ去られ気づいたときには半分以上のテストが失敗することも珍しくない。そうだ最近Web業界ではCypressというE2Eテストのツールが流行っており、僕らも導入している_

当時は新しい技術の体験と隊の明るい展望が見えたので隊員の誰もがCypressがきっと平和をもたらすと思い込んでた...

## 衛生兵の対症療法
Vue.jsが装備された我らの中隊はのSafariのエリアで感染症（バグ）をかかってしまった。厄介なことに装備の問題なのか担当するエリア問題なのか原因が特定できず衛生兵は困っていた。

運良く対症療法的な治療法で治す方法をみつけて処置に当たった。感染者が多いので衛生兵に負荷がかかり逆に処置ミスをしないか心配だった。

> _処置ミスとはIT業界でいうコピペミスとよく似ている_

> 僕らも似たような経験をしたことがある。原因不明のバグを修正する際にやむを得ず対症療法的な方法で対処することもある。その対処がもっと大きな問題を引き起こすことは少なくない。

問題なくすべての処置が終わると現場にほっとした雰囲気と隊員たちに笑顔が戻った。しばらくすると隊を率いる中尉から出発命令が出て進むことになった。

> _すべて順調そうに見えたがこのあとの恐怖は誰も予想できなかった..._

`中尉、隊員が次々と倒れて行きました！！` との無線通信が先頭の分隊から入った。見に行くと倒れているのは感染して治療を受けた隊員全員だったのがわかった。`あの時は俺の頭の中は真っ白だった` と後に中尉が話してくれた。

> _この真っ白というのはIT業界での修正箇所が多いときに動作確認が漏れたままデプロイすると画面が真っ白なになってシステム停止することとよく似ている_

すぐ別の衛生兵がやってきて処置ミスに気づき、すぐ解毒薬を飲ませた。おかげで全員命を落とすことなく回復した。

> _しかし、対症療法の影響はこれだけでは終わらなかった..._

## 予防接種の義務化
`こんな状況でミスは仕方ないがなんとか事前に防ぐ方法ないのかね` と中尉がぼそっと発言した。Cypressならなんとかできそうと思った隊員がアイデアを説明してくれた。

治療する際に先ず隊の半分に服用させそれからCypressで検査して経過観察をする。問題なければ残り半分にも服用させれば全員致命傷になることは防げるというものだった。

> _これはIT業界でいうBlue Greenデプロイメントで障害起きてもサービスを継続できる仕組みとよく似ている。具体的には待機環境にデプロイしてみてCypressなどE2Eテストツールで検証し問題なければ待機を本番へと昇格させるという方法。_

`これをやってくれ` と中尉は命令を出し、隊員にミッションを与えた。

## 本当に伝えたいこと
映画と現実の世界さまよいながらようやく本当に伝えたかった `Cypressをガチ運用した時の話し` になる。

### configの上書く方法

このプロジェクトではCypressをモックサーバーで動かすようにしているためcypress.jsonで待機環境へのHOST情報など共有できなかった。本番用の場合別のシェルスクリプトを用意しENVで設定情報を上書きするようにした。デプロイ担当者が待機環境へデプロイしたあとにE2Eを実行し致命的機能の動作検証を行う。

設定例：

```sh
export CYPRESS_SOME_TOKEN=aabbcc
export CYPRESS_baseUrl=https://swap.awesome-app.com
export CYPRESS_browser=chrome
export CYPRESS_defaultCommandTimeout=30000

# 必要なテストだけ実行する
npx cypress run --spec e2e/critical-test.spec.js
```

詳細：
https://docs.cypress.io/guides/references/configuration.html#Environment-Variables


## とある隊員の不満
`日々の訓練にCypress検査を盛り込まないと意味ないじゃん` って別の隊員が不満をこぼした。Cypress検査とはある種の運動みたいなもので一度やれば良いというものではない。日々の訓練に盛込んで新しい菌（バグ）も検査できるようにしないと意味ないからだ。

> _これはIT業界でいうCI、すなわちソースコードに変更を加える度にテストを実行し検証を行う仕組みとよく似ていて、今やCIがないとありえないほど普及している_

それを聞いた分隊リーダーである伍長が中尉に相談しに向かった。今のところ緊急的な任務もないので中尉も快く日々の訓練にCypress検査を取り入れることを許諾してくれた。
さて、これはいつまで続くのだろうか。上層部の圧力がかかるとまたCypress検査を置いてきぼりすることになるかもしれないと心のなかでつぶやいた。

## 日々の訓練
しばらくぶりにCypress検査を日々の訓練に盛込むと検査項目と訓練が合わなく菌がなくて検査に引っかかる。先ずCypressの検査項目を最新の訓練に合わせる必要がある。

> _これはIT業界でいうとしばらくメンテナンスされなくなったテストコードをCIに乗せるとテストが失敗する事象と似ている。テストコードはソースコード一緒にメンテナンスすべきなのだ_

## 検査用の仕組みを構築
検証用の仕組みとはCypressをCIに組込み日々の開発のフローに乗せるのである。

### CypressをCIで運用する

参考：
https://docs.cypress.io/guides/guides/continuous-integration.html#Setting-up-CI

#### イメージ構築
CypressはGPU処理が必要なのでヘッドレス環境で設定が困難。Cypress公式のDockerイメージを使うのがベスト。
https://docs.cypress.io/examples/examples/docker.html#Images


#### モックサーバーの起動を待つ
CI上でモックサーバーの起動とJSのビルドを実行し、その準備ができてからCypressのテストを流す必要がある。公式ドキュメントのおすすめでwait-onパッケージを使って待機するようにした。

下記の２つを並列で実行：

 - モックサーバーの起動とJSのビルド
 - index.jsを待機しアクセス可能になったらCypressを実行

起動コマンド：

```
yarn e2e:ci
```

package.jsonの例：

```json
{
  "scripts": {
    "e2e:ci": "DEV_VUE_STRICT_MODE=0 run-p serve e2e:ci:test",
    "e2e:ci:test": "run-s serve:wait cy:run:ci serve:kill",
    "serve": "./start-mock-server",
    "serve:kill": "kill $(lsof -i :8081 | grep node | awk '{print $2}')",
    "serve:wait": "wait-on http-get://localhost:8080/index.js",
    ...
  }
}
```

`e2e:ci`：パラレルでモックサーバー起動、テストの実行
`e2e:ci:test`：シリアルでサーバー起動の待機、テストの実行、モックサーバーの停止をする
`serve:kill`：mockサーバーのプロセスを停止
`serve:wait`：wait-onモジュールでindex.jsを待機

run-s, run-p はこちらを参照：
https://github.com/mysticatea/npm-run-all

#### タイムゾーン
CI上ではデフォルトUTCで実行されされているせいで期間や時間検証のテストが失敗する。Dockerイメージにタイムゾーンを設定することで解決。

```
RUN apt-get update \
    && apt-get install -y \
    tzdata \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    mkdir -p /usr/share/zoneinfo/Asia && \
    cp /etc/localtime /usr/share/zoneinfo/Asia/Tokyo
```

#### リトライ
UIテストのためCIの体力の影響でサーバー応答が遅延すると時々テストが失敗することがある。１回でもテストが失敗するとCIタスク自体が失敗と判断されてしまい再度実行する必要が出てくる。解決策としては実行単位はディレクトリー単位とし失敗したディレクトリー配下のテストだけを３回までリトライさせるようにした。

例：

```sh
#!/usr/bin/env bash
# set -x

CURRENT_PATH="`dirname \"$0\"`"
PROJECT_PATH="`( cd \"$CURRENT_PATH/../..\" && pwd )`"
RETRY_COUNT=3
has_error=0
targets=''

# テストファイルを指定してCypressを実行
function run_cypress {
  npx cypress run --spec "$1"
  if [ $? -eq 0 ]; then
    return 0
  fi
  return 1
}

# 実行とリトライ
function run_test_and_retry {
  run_count=0
  result=1
  while [ $run_count -lt $RETRY_COUNT ]; do
    ((run_count++))
    spec=$1

    run_cypress $spec
    result=$?

    if [ $result -eq 0 ]; then
      return 0
    fi

    if [ $run_count -eq $RETRY_COUNT ]; then
      return 1
    fi

    echo "Retry test for $spec"
  done
}

# テストが入っているディレクトリーのリストアップ
for directory in `find e2e/* -type d`
do
  result=`find $directory -regex ".*.spec.js"`

  # Add directory of specs found
  if [ ! -z "$result" ]; then
    targets="$targets\n$directory"
  fi
done

# ディレクトリー単位でテストを実行
for spec in `echo -e $targets`
do
  # *.spec.jsパターンのみテストとみなす
  run_test_and_retry "$spec/**/*.spec.js"
  run_test_result=$?

  # 失敗した場合エラーフラグを立てておく
  if [ $run_test_result -ne 0 ]; then
    # Error flag for exit code
    has_error=1
    echo "Error occurs"
  fi
done

if [ $has_error -ne 0 ]; then
  echo "Has error."
fi

exit $has_error
```

#### console.logが邪魔をする
例えばVueのstrictモードがオンの場合によって大量をconsoleに書き出すことがあり、その影響でCypressが遅くなる。CIの場合connsoleログは不要なので環境変数で切り替える。

storeの実装例：

```js
new Vuex.Store({
  strict: process.env.DEV_VUE_STRICT_MODE === '1',
  ...
})
```

タスクの例：
package.json

```
"e2e:ci": "DEV_VUE_STRICT_MODE=0 run-p serve e2e:ci:test",
```

_それよりアラートを直せばいいじゃんか
（そんなこと分かっとんねん。だけどできないのが現実）_

### Railsとの連携（未実装）
Cypressにはexec関数というSystem上でコマンドを実行できる仕組みがあり、DBにシードデータの投入などに使われている模様。バックエンドのRailsなども別のコンテナーで実行されている場合はコンテナー間の通信が必要になる。
これに関しては調査中で解決できていない。

詳細：https://docs.cypress.io/api/commands/exec.html#Syntax

RailsとCypressを同じコンテナーに用意できると解決なので引き続きチャレンジしていきたい。

## ミスの検知
検査場の構築が完了すると検査項目と訓練内容をあわせる。一番手っ取り早い方法としては検査してみて引っかかった項目だけを治す。すると以前治療ミスだった処置がCypress検査で引っかかったのだった。
これでCypress検査の重要性が一段と高まった。

 >_IT業界ではテストを書いてみるとびっくりするほどバグをたくさん発見する、しかも内容はスペルミスやコピペミスなど人的ミスが多い_

やはり検査というのは正常であることを知るより正常でないことを知ることが重要になる。ずっと正常のままだと検査内容が不足していることもあるので定期的な見直しが必要。

> _IT業界でもテストコードが正常だから安心することが多いがそもそも新規画面の場合テストが記述されないと正常のままになるのだ。その場合カバレッジをとって定期的に見直す必要がある。しかしCypressの場合要件が網羅されているかを比較する材料がないので機械的にカバレッジを取得できないのである_

## ある日の朝礼
`振り返って見ると我々の隊も大分様変わりしたようだ。怪我したもの、病気になったもの、やむを得ず後方支援に回ったものもいた。だが諸君はここまでよく付いてきてくれた。諸君の忍耐、努力、志しに感謝し誇りに思う。これからもさらなる苦痛や恐怖がまっているのであろう。だが我々は進む、最後の人が命を落とすまで。` とスピーチした中尉をみて涙を流しているとなり2等兵を僕はみていた。

> _IT業界に身をおいて約12年。様々なツールや言語を学び業務をこなしてきた。辛いことも、達成感もある。これからも学び続けるのであろう。今回はCypressの導入、実装、運用を1からすべて携われて感謝したい。Cypressがすべての問題を解決してくれる訳ではないがきちんと運用すればエンジニアの負荷は軽減されるはずだ。_