Github Actionsでお手軽にIEのE2Eテストを実行する
Posted at: 2020-07-19
今更ながら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だけでテストすることはあまりないと思うので他のブラウザでも実行してみる。
まずはとりあえず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から登録しておく。
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.')
})
アプリケーションと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 でキャッシュを使った高速化 - 生産性向上ブログ