GitHub Pagesをビジュアルリグレッションテストをしてみた

# モチベーション
こんにちは@glassmonkeyです。
Github ActionといえばCI。
せっかくCIするなら画像使ったものをやってみたかったので、簡単にビジュアルリグレッションテストをやってみました。
突貫で作ったのでご意見ご感想あればありがたいです。

# ビジュアルリグレッションテストとは

[アスタミューゼ様](https://tech.dely.jp/entry/vis_reg_test)のブログでの説明がわかりやすかったので引用させていただきます。
> ビジュアルリグレッションテストとは視覚的な回帰テストのことで、具体的にはスクリーンショットを撮影して差分抽出して行うテストです。

つまりGithub Actionの環境でビジュアルリグレッションテストを行うことは
仮想環境上で以下の画像を生成することになります。 
 * 変更前と変更後のスクリーンショット
 * 変更前と変更後の差分画像

この記事では
* クロール及びスクリーンショットは[Selenium](https://selenium.dev/) + [Headless Chrome](https://chromium.googlesource.com/chromium/src/+/master/headless/README.md) 
* 差分画像生成（画像処理）には [OpenCV](https://opencv.org/)
を扱いました。

# どのようにしたか

## サンプル
実際作ってみたものが下記にあたります。
https://github.com/glassmonkey/vue-sample/pull/3
私のVuejsの勉強のために作ったアプリケーションに追加してみました。

今回は下記のように変更点がわかりやすく矩形で囲ってみました。

| 元画像  |変更後画像 |差分画像    |
|:-----------------|------------------|------------------|
| <img width="600px"# alt="元画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152938/136950cf-4133-b4c5-badb-6ed751650df1.png">           |              <img width="600px" alt="変更画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152938/6b2e7503-b756-890f-96a1-d5a4052c527d.png">|  <img width="600px" alt="差分画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152938/f60db6b6-0687-0f5b-c0e5-9f13dd5cd78a.png">      |

## 要件
* 変更前は本番環境(github pagesでデプロイする想定)
    * デプロイは @peaceiris さんの[GitHub Actions による GitHub Pages への自動デプロイ](https://qiita.com/peaceiris/items/d401f2e5724fdcb0759d)を使用させていただきました。
* 変更後はPRの内容(CI上の仮想環境でdocker-composeで起動させる)

https://github.com/glassmonkey/vue-sample/blob/master/.github/workflows/test.yml#L19-L20

```yml

BASE_URL: https://glassmonkey.github.io/vue-sample

DIFF_URL: http://localhost:8080
```


# テストの内容
## テストの流れ
テスト用のymlは下記になります。
https://github.com/glassmonkey/vue-sample/blob/master/.github/workflows/test.yml

```yml
name: test

on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1 # ①
      - name: develop run # ②
        run: |
          docker-compose up -d
      - name: run test # ③
        run: |
          cd tests && \
          docker-compose build && \
          docker-compose run app \
        env:
          WINDOW_SIZE: 1024,768
          BASE_URL: https://glassmonkey.github.io/vue-sample/
          DIFF_URL: http://localhost:8080
      - uses: jakejarvis/s3-sync-action@master # ④
        env:
          AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: 'ap-northeast-1'
          SOURCE_DIR: './tests/dist'
          DEST_DIR:  ${{github.repository}}/${{github.sha}}
      - name: post maessage # ⑤
        run: |
          cd tests && bash post.sh
        env:
          S3_PATH: https://${{ secrets.AWS_S3_BUCKET }}.s3-ap-northeast-1.amazonaws.com/${{github.repository}}/${{github.sha}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
```

認証情報はGithub上`settings > secrets`から下記の通り設定させる必要はあります。
<img width="776" alt="スクリーンショット 2019-12-02 12.30.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152938/5b0b7155-3969-f396-de5e-f418e96f043e.png">


簡単に解説すると

1. 初期化 
2. PRの内容で仮想環境を構築
3. ビジュアルリグレッションテスト用コンテナの構築及び実行
4. 生成した画像をs3にアップロード
5. PRに画像をポスト

の流れになります。３の画像生成と5のPRにポストの内容に関して解説します。

## 画像生成
スクーンショットの撮影および、差分画像生成は下記で行っています。
https://github.com/glassmonkey/vue-sample/blob/master/tests/src/main.py



今回テスト用に実行するコンテナはローカルで構築したアプリとは独立させたので、localhostの名前解決ができないので下記で無理やりテスト側で書き換えました。

```python
    if "localhost" in url: 
         host_addr = subprocess.run(["ip route | awk 'NR==1 {print $3}'"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).stdout.decode("utf8").rstrip('\n')
         url = url.replace("localhost", host_addr)
```

### スクリーンショットについて

[Docker上でSeleniumとHeadless ChromeとPython3を動かす](https://qiita.com/sikkim/items/447b72e6ec45849058cd)の記事を参考にさせていただきました
特筆すべき点としては下記のオプション項目は調整必須な点です。
特にヘッドレスクロームで起動する場合は起動字のタイミングでウィンドウサイズ指定が必須な点は少しハマりました。

```python
   options.add_argument('--headless')
   options.add_argument('--no-sandbox')
   options.add_argument('--disable-dev-shm-usage')
   options.add_argument('--hide-scrollbars')
   options.add_argument('--window-size={}'.format(os.environ['WINDOW_SIZE']))
```

今回は下記のようにURLを静的にスクリーンショットにしましたが、スクリーンショット対象のDom要素の指定などもできるようなので外部からカスタマイズできるようにしてもいいのかなとは思いました。

```python
    driver.get(url)
    driver.save_screenshot(filename)
```

### 差分画像生成について
[こちら](https://www.pyimagesearch.com/2017/06/19/image-difference-with-opencv-and-python/)の記事を参考にさせていただきました。 下記の関数でスクリーンショット画像の差分画像を生成するようにしています。

```python
def diff_images(base_image_path, diff_image_path):
    """
    referer to https://www.pyimagesearch.com/2017/06/19/image-difference-with-opencv-and-python/
    :param base_image_path:
    :param diff_image_path:
    :return:
    """
    # load the two input images
    base_image = cv2.imread(base_image_path)
    diff_image = cv2.imread(diff_image_path)

    # convert the images to grayscale
    grayA = cv2.cvtColor(base_image, cv2.COLOR_BGR2GRAY)
    grayB = cv2.cvtColor(diff_image, cv2.COLOR_BGR2GRAY)

    (score, sub) = compare_ssim(grayA, grayB, full=True)
    sub = (sub * 255).astype("uint8")
    print("SSIM: {}".format(score))
    thresh = cv2.threshold(sub, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = imutils.grab_contours(cnts)

    for c in cnts:
        # compute the bounding box of the contour and then draw the
        # bounding box on both input images to represent where the two
        # images differ
        (x, y, w, h) = cv2.boundingRect(c)
        cv2.rectangle(base_image, (x, y), (x + w, y + h), (0, 0, 255), 2)
        cv2.rectangle(diff_image, (x, y), (x + w, y + h), (0, 0, 255), 2)
    cv2.imwrite("/app/dist/base.png", base_image)
    cv2.imwrite("/app/dist/diff.png", diff_image)
    cv2.imwrite("/app/dist/sub.png", sub)
```

## PRへPOST
https://github.com/glassmonkey/vue-sample/blob/master/tests/post.sh

[s3にアップロードするアクション](https://github.com/jakejarvis/s3-sync-action)を用いてs3にアップした画像をGithubのApi経由でアップさせた画像をPostしています。
処理には[jessfraz/shaking-finger-action](https://github.com/jessfraz/shaking-finger-action/blob/master/add-comment.sh)を参考にしました。

※ 今回使用したバケットの設定は適当に作ったので割愛とさせていただきます。
コミットの度に画像を再生成する仕様なのでコミットのSHAでディレクトリ別にはしています。

## 結果
このPRに無事に投下されました。
https://github.com/glassmonkey/vue-sample/pull/3
<img width="610" alt="スクリーンショット 2019-12-02 12.49.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152938/3983834d-eac3-8fca-cb53-bf2d66740f53.png">



# まとめ
簡単なビジュアルリグレッションテストをおこなうことができた。
せっかかくなのでちゃんとactionとして別リポジトリにしてもいいかもしれない。
まともにGithubのApiを触ったことがなかったので画像のPostをどうしようかすごい悩んだ。
今回はs3にしましたが、別のアプローチもありかもしれない。
Opencvのインストールが煩雑になるので断念したんですが、遅いのでテスト用コンテナをalpineから諦めましたがalpineにしたいですね。