zenpachi blog

Github Actionsでお手軽にIEのE2Eテストを実行する

このエントリーをはてなブックマークに追加

今更ながらGithub Actionsを使ってみようとして調べていたらコンテナにWindows環境を選べるとのことで、IEでのE2Eテストが実行できるらしいので試してみたメモ。

結論から言うとIEでE2Eテストを実行すること自体は一瞬でできた。
どちらかというと実際のプロダクトで使う場合を想定していろいろドキュメントを読んだり設定をいじってみたりしたのがメイン。

E2EのテストランナーとしてはTestCafeを使用。

とりあえずIEでTestCafeを走らせてみる

TestCafeのドキュメントにもGithub Actionsでの実行方法は書いてある。
Integrate TestCafe with GitHub Actions | TestCafe

↑ではTestCafeが公式で用意しているGithub Actionを使用しているが、runnerの部分はカスタマイズしたくなることが多いのでなんとなく公式を参考にしつつも自前でスクリプトを書いてみることにする。

使うのはtestcafe@action/coreのみ

$ npm i testcafe @action/core --save-dev

公式によるとそのリポジトリ内でしか使われないactionsは.github/actions配下に置くことを推奨しているがひとまずrootに置いておく。

name: "E2E tests example"
description: "E2E testing by TestCafe"

inputs:
  browser:
    description: 'Browser used by TestCafe'
    required: true

runs:
  using: 'node12'
  main: 'index.js'

内容的にはworkflowからbrowserを指定してTestCafeが動くだけ。

const core = require('@actions/core')
const createTestCafe = require('testcafe')

async function main() {
  const testCafe = await createTestCafe()
  try {
    const runner = testCafe.createRunner()
    const browser = core.getInput('browser')

    const failedCount = await runner
        .src('./tests/')
        .browsers(browser)
        .run()

    if (failedCount) throw new Error(`Failed count: ${failedCount}`)
  } catch (e) {
    core.setFailed(e.message)
    process.exit(1)
  } finally {
    await testCafe.close()
  }
}

main()

process.exit(1) は本来必要ないはずだが、TestCafeを使うとcore.setFailed()を行ってもなぜかactionがsuccessになるので入れている。ここはよくわかってない。

on: [push]

jobs:
  e2e-IE:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install Dependencies
        run: npm ci
      - name: E2E testing
        uses: ./
        with:
          browser: IE

用意したサンプルはこんな感じ。TestCafeの公式サイトのtitleを取ってくる。

import { Selector } from 'testcafe'

fixture('test sample')
  .page('https://devexpress.github.io/testcafe/example/')

test('get title', async t => {
  const title = Selector('title').textContent
  await t
    .expect(title).eql('TestCafe Example Page')
})

git push のたびに実行されるようにしているのでこのリポジトリをpushすればテストが実行される。
もうちょっと設定したり詰まったりすることがあるかと思ったが一瞬で実行できてしまった。

IEでTestCafeを走らせる

マトリクス

IEだけでテストすることはあまりないと思うので他のブラウザでも実行してみる。
まずはとりあえずjobを増やす。

on: [push]

jobs:
  e2e-IE:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install Dependencies
        run: npm ci
      - name: E2E testing
        uses: ./
        with:
          browser: IE
  e2e-Chrome:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install Dependencies
        run: npm ci
      - name: E2E testing
        uses: ./
        with:
          browser: chrome

これでIEとChromeでE2Eテストを並列に実行してくれる。
ただ、ほぼ同じjobを2回書くのが面倒だしメンテナンス性が悪い。これは「build matrix」という機能をを利用することで解消できる。

on: [push]

jobs:
  e2e-xbrowser:
    name: e2e-${{ matrix.browser }}
    strategy:
      matrix:
        browser: [IE, chrome]
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install Dependencies
        run: npm ci
      - name: E2E testing by ${{ matrix.browser }}
        uses: ./
        with:
          browser: ${{ matrix.browser }}

pushするとこんな感じ。

マトリクステスト

テスト内容的に並列で実行されると都合が悪い場合、jobを2回書くことにはなるがneedsを使用してjobを順番に実行することもできる。

on: [push]

jobs:
  e2e-IE:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install Dependencies
        run: npm ci
      - name: E2E testing by IE
        uses: ./
        with:
          browser: IE
  e2e-Chrome:
    needs: e2e-IE
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install Dependencies
        run: npm ci
      - name: E2E testing by Chrome
        uses: ./
        with:
          browser: Chrome

テストするアプリケーションのdeploy

Github Actions内でテスト対象とするアプリケーションをDockerでE2E用にに立ち上げることができるとGithub内で完結するのでベストだったが、ドキュメントを読む限りGithubホストランナーのOSがubuntu-latestじゃないとサービスコンテナは動かない
OSがWindowsなのでホストランナー内でDockerでLinuxベースのコンテナを立ち上げることもできないし、複数のjobsを連携させることもできないっぽい?

Windowsのホストランナー内に入っているDBを使ったり、必要なものを自前でインストールしていけばできないこともない気がするが設定が増えてメンテナンスが大変になるし、本番環境と同じ構成ではないのでてテストとしての意味が薄れてしまう。

なのでE2Eのjobを走らせる前にどこかにアプリケーションをデプロイする方法を試す。
できればブランチごとに専用の環境が用意されるのが理想なので、このあたりはHerokuのReview Appsあたりと組み合わせるといい感じになりそうな気がするが試していない。

とりあえずexpressのみのシンプルなアプリケーションを作ってHerokuにデプロイをしてからテストを行ってみる。
(HerokuへのデプロイはGithub Actionsを使用しなくても直接HerokuとGithubを連携させてしまえばいいが今回はGithub Actionsを使ってのdeployを試してみる)

サンプルとなるアプリケーションを/app配下にexpressでサクッと作る。

{
  "name": "docker_web_app",
  "version": "1.0.0",
  "description": "Node.js on Docker",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.16.1"
  }
}
'use strict'

const express = require('express')

const PORT = process.env.PORT || 8080
const HOST = '0.0.0.0'

const app = express()
app.get('/', (req, res) => {
  res.send('<h1>Hello World!</h1>')
})

app.get('/hoge', (req, res) => {
  res.send('<h1>This is hoge page.</h1>')
})

app.listen(PORT, HOST)
console.log(`Running on http://${HOST}:${PORT}`)
FROM node:12
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "node", "server.js" ]
node_modules
npm-debug.log

Herokuにデプロイするjobをworkflowに追加する。actionは公開されているものをそのまま使うことにした。
E2Eテストはデプロイ後に実行されるようにし、test側もbase_urlを受け取れるように修正する。

HEROKU_API_KEYはgithubのsettingから登録しておく。

HEROKU_API_KEYを登録

on: [push]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: akhileshns/heroku-deploy@v3.2.6
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: "YOUR APP's NAME"
          heroku_email: "YOUR EMAIL"
          usedocker: true
          appdir: app
  e2e-xbrowser:
    needs: deploy
    name: e2e-${{ matrix.browser }}
    strategy:
      matrix:
        browser: [IE, chrome]
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install Dependencies
        run: npm ci
      - name: E2E testing by ${{ matrix.browser }}
        uses: ./
        with:
          browser: ${{ matrix.browser }}
          base_url: "YOUR APP's BASE URL"
...
inputs:
  browser:
    description: 'Browser used by TestCafe'
    required: true
  base_url:
    description: 'Tests pages base url'
    required: true
...
import { Selector } from 'testcafe'
const core = require('@actions/core')

fixture('top page')
    .page(core.getInput('base_url'))

test('get h1 text', async t => {
    const text = await Selector('h1').textContent
    await t
        .expect(text).eql('Hello World!')
})
import { Selector } from 'testcafe'
const core = require('@actions/core')

fixture('hoge page')
    .page(`${core.getInput('base_url')}hoge`)

test('get h1 text', async t => {
    const text = await Selector('h1').textContent
    await t
        .expect(text).eql('This is hoge page.')
})

Herokuへのdeploy

アプリケーションとE2Eのリポジトリを分ける

アプリケーションとE2Eを別のリポジトリで開発している場合。 E2Eリポジトリにアクションを定義して、アプリケーション側のリポジトリからworkflow内で読み込んで使用する。

ここまで作っていたリポジトリから/app配下のファイルと.githubディレクトリをアプリケーション用の別リポジトリに移して削除する。 また、E2Eリポジトリではreleaseブランチを作り、node_modulesもcommitに含めたうえでv0.0.1など適当にtagを付けておく。

アプリケーション用のリポジトリの構成は以下。

root/
 ├ .github/
 │ └ workflows/
 │   └ main.yml
 ├ .gitignore
 ├ .dockerignore
 ├ Dockerfile
 ├ package.json
 └ server.js

E2Eリポジトリ内のアクションを使用できるようにワークフローを修正する。

deployジョブからappdirを削除し、e2eジョブではE2Eリポジトリをチェックアウトする。
また、tokenにはE2EリポジトリにアクセスできるPersonal access tokensをsecrets経由で取得する。

on: [push]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: akhileshns/heroku-deploy@v3.2.6
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: "YOUR APP's NAME"
          heroku_email: "YOUR EMAIL"
          usedocker: true
  e2e-xbrowser:
    needs: deploy
    name: e2e-${{ matrix.browser }}
    strategy:
      matrix:
        browser: [IE, chrome]
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - name: Checkout e2e repository
        uses: actions/checkout@v2
        with:
          repository: "YOUR E2E REPOSITORY"
          ref: v0.0.1
          token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
          persist-credentials: false
          path: ./
      - name: E2E testing by ${{ matrix.browser }}
        uses: ./
        with:
          browser: ${{ matrix.browser }}
          base_url: "YOUR APP's BASE URL"

これでE2Eリポジトリがプライベートリポジトリであっても、アクションを呼び出して実行することができる。

参考: GitHub - actions/checkout: Action for checking out a repo

IPでのアクセス制限

E2E用のアプリケーションにIPでのアクセス制限をかけたい場合。ドキュメントによると

WindowsとUbuntuのランナーはAzureでホストされ、IPアドレスの範囲がAzureデータセンターと同じになります。 現在、すべてのWindows及びUbuntuのGitHubホストランナーは、以下のAzureリージョン内にあります。

Microsoftは、AzureのIPアドレスの範囲をJSONファイルで毎週更新しています。このファイルは、Azure IP Ranges and Service Tags - Public Cloud (AzureのIPアドレス範囲とサービスタグ - パブリッククラウド)のウェブサイトからダウンロードできます。

とのこと。これを使えば多分できると思うがやや面倒なので試してはいない。

参考: GitHubホストランナーの仮想環境 - GitHub Docs

依存関係のキャッシュ

依存するパッケージのダウンロードによってジョブの実行時間が長くなるのを避ける。
現状はE2Eリポジトリのv0.0.1にはnode_modulesを含めているのでパッケージのダウンロードは発生しないので、キャッシュ機能を試すためにnode_modulesを含めていない状態にするためcheckout時にrefを指定せずnpm ciを使用するようにしたうえで、actions/cacheを使用する。

on: [push]

jobs:
  deploy:
    ...
  e2e-xbrowser:
    ...
    steps:
      ...
      - name: Checkout e2e repository
        uses: actions/checkout@v2
        with:
          repository: "YOUR E2E REPOSITORY"
          token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
          persist-credentials: false
          path: ./
      - name: Get npm cache directory
        id: npm-cache
        run: echo "::set-output name=dir::$(npm config get cache)"
      - uses: actions/cache@v2
        with:
          path: ${{ steps.npm-cache.outputs.dir }}
          key: ${{ env.cache-version }}-${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      - name: Install Dependencies
        run: npm ci
     ...

OSごとにnpmのキャッシュディレクトリは異なるのでnpm-cacheでキャッシュディレクトリを取得してそれをキャッシュのkeyに含める。
これでpushを実行すると2回目以降はキャッシュを復元するので時間の短縮になる。

と思ったのだが、確かにキャッシュを利用してInstall Dependenciesの時間は短縮されているがキャッシュの復元に異常に時間がかかるようになってしまいトータルの時間はむしろ増えてしまった……

もう少し調査の必要がありそう。

参考: GitHub Actions でキャッシュを使った高速化 - 生産性向上ブログ

データの永続化

E2Eテスト実行時にスクリーンショットや動画を記録し、それを永続化する。 まずはE2Eリポジトリ側のアクションを動画が撮影できるように修正。

ffmpegはnpm install実行時にplatformによってインストールされるバイナリファイルが異なるので、あらかじめインストールはせずに、videoが有効だった時だけ実行時にインストールする。 こちらはreleaseブランチを作ってv0.0.2タグをつける。

const core = require('@actions/core')
const { execSync } = require('child_process')
const createTestCafe = require('testcafe')

if (core.getInput('video') == 'true') {
  execSync(`npm i @ffmpeg-installer/ffmpeg`, { stdio: 'inherit' })
}

async function main() {
  const testCafe = await createTestCafe()
  try {
    const runner = testCafe.createRunner()
    const browser = core.getInput('browser')

    if (core.getInput('video') === 'true') runner.video('outputs/videos/')
    ...
  }
  ...
}

main()
...
inputs:
  ...
  video:
    description: 'Record videos'
    default: false
...

次にアプリケーションブランチのworkflowを修正。 actions/upload-artifact@v1を使って動画ファイルを永続化する。IEではTestCafeのvideo機能が使えないのでifを使用してIEの時は実行しない。

on: [push]
env:
  cache-version: v1

jobs:
  deploy:
    ...
  e2e-xbrowser:
    ...
    env:
      e2e-video: false
    steps:
      - name: Checkout e2e repository
        uses: actions/checkout@v2
        with:
          repository: "YOUR E2E REPOSITORY"
          token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
          persist-credentials: false
          ref: v0.0.2
          path: ./
      - name: E2E testing by ${{ matrix.browser }}
        uses: ./
        with:
          browser: ${{ matrix.browser }}
          base_url: "YOUR APP's BASE URL"
          video: ${{ env.e2e-video }}
      - name: Archive E2E videos
        if: ${{ env.e2e-video  == 'true' && matrix.browser != 'IE' }}
        uses: actions/upload-artifact@v1
        with:
          name: e2e-videos
          path: outputs/videos

これでpushするとE2Eテスト時の動画を保存するようになる。保存された動画ファイルはGithubのActionsページからDL可能。
E2Eの動画ファイルは実際のプロダクトではかなり重くなるので、コストを抑えるためにS3などにアップロードしてGithubのstorageを使用しないようにした方がよいと思われる。

データの永続化

感想とか

  • TestCafeはCI上のMacOS 10.15をサポートしていないのでMacでのSafariのテストがまだできない
  • job間の連携ができたり、ホストランナーがWindowsでもサービスコンテナを使えるようになったりするとアプリケーションの立ち上げもGithub内で完結するのでさらに手軽になるので期待
  • 他のツールを組み合わせずにGithub内でサクッと処理がかけて実行できるのは便利
  • 個人開発などで使うには十分だが、E2Eの結果の見え方など業務プロダクトのE2Eを乗り換えるにはもう少し改善が必要そうなので引き続き調査する

参考

GitHub Actionsのドキュメント - GitHub Docs
Integrate TestCafe with GitHub Actions | TestCafe
GitHub - actions/checkout: Action for checking out a repo
GitHub Actions でキャッシュを使った高速化 - 生産性向上ブログ

このエントリーをはてなブックマークに追加