E2Eテスト: Github Actions上でTestCafeを試す(PCブラウザ編)

はじめに

こんにちは、ラクマの@itinaoです。

E2Eテストについて、概要からお手軽に試す方法までを全5編で記載しています。

  1. E2Eテスト: 導入の必要性・何を導入するのか
  2. E2Eテスト: TestCafeを試す
  3. E2Eテスト: Github Actions上でTestCafeを試す(PCブラウザ編) ← 今回はココ
  4. E2Eテスト: Github Actions上でTestCafeを試す(モバイルブラウザ編)
  5. E2Eテスト: Selenium Gridを試す

前回の記事で、TestCafeをローカルの環境で実施する手法を記しました。

ローカルで実行できるだけでも便利ですが、 任意のタイミングで自動実行できるようになると更に便利です。

今回はお手軽に実施する方法として、Github Actionsを使った方法を説明していきます。

Github Actionsとは

Github Actionsとは、GitHub上で動作するサーバレス実行環境のことです。

Actionの実体はDockerコンテナで、 ユーザはGitHubのインフラ上で任意のDockerイメージからコンテナを起動し、その中で任意のコマンドやスクリプトを実行できます。

リポジトリのデータがコンテナ内にマウントされた状態になるので、コマンドやスクリプトからソースコードに直接アクセス可能です。

詳細はこちらをご参照ください。

https://docs.github.com/ja/actions

Github Actions上で動作するOS

実行コストやEmulator等についても記載していますが、これらのサーバーが扱えます。

実行コストについての詳細は、こちらをご参照ください。 macosでたくさん実行するのは要注意です。

https://docs.github.com/ja/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions

どのAndroid Emulator / iPhone Simulatorが使えるかの詳細は、こちらをご参照ください。

https://github.com/actions/virtual-environments#available-environments

Github Actions上でTestCafeを実行する

今回はこのようなOSの区分けで、E2Eテストを実行していきたいと思います。

最終的なディレクトリ構成はこの様になります

※ package.jsonや、yarn run devで動く対象以外

$ tree e2e/ .github/
e2e/
├── run_mobile_chrome.sh
├── run_mobile_safari.sh
├── run_safari.sh
├── runner.js
└── src
    └── index
        └── index.ts
.github/
└── workflows
    └── test-e2e.yml

実行する内容

  • Github Actions上でWEBサーバーを起動する
  • Github Actions上で起動しているWEBサービスに対してE2Eテストを行う

もちろん、 PublicなWEBサービスについてもアクセスできるので、その場合はWEBサーバー起動部分はないものと補完して頂ければと思います。

Windows: Microsoft Edge / Google Chrome / Firefox

下記は失敗する例です。。

.github/workflows/test-e2e.yml を追加します。

name: E2E Test

on: [push]

jobs:
  install-apps:
    name: install
    strategy:
      matrix:
        os: [windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v2
      - name: Cache node modules
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
      - name: yarn install # TestCafeもここでインストールしているものとする
        run: yarn install --frozen-lockfile

  test-windows:
    name: Run tests across latest browsers on windows
    strategy:
      matrix:
        browser: ['edge', 'chrome', 'firefox']
    runs-on: windows-latest
    timeout-minutes: 20
    needs: [install-apps]
    steps:
      - uses: actions/checkout@v2
      - name: Cache node modules
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
      - name: Run TestCafe
        run: |
          yarn run dev > /dev/null 2>&1 & # サーバー起動&バックグラウンドへ
          yarn run testcafe ${{ matrix.browser }} # テスト開始
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: reports-windows-${{ matrix.browser }}
          path: reports/

これと同じ書き方をMac上で実行すると成功するのですが、Windowsではうまくいきませんでした。

下記の部分に curl を挟むと分かりやすいのですが、 Macの場合はサーバーが起動するまで curl は待機しますが、 Windowsの場合は curl: (7) Failed to connect to localhost port 3035: Connection refused となって即座にエラーとなります。

      - name: Run TestCafe
        run: |
          yarn run dev > /dev/null 2>&1 & # サーバー起動&バックグラウンドへ
          curl http://localhost:3035
          yarn run testcafe ${{ matrix.browser }} # テスト開始

ということで、起動するまで待つようにしました。

Windowsのパワーシェルでスクリプトを書く力量がなかったので、Node.jsを用いてます。

  test-windows:
    name: Run tests across latest browsers on windows
    strategy:
      matrix:
        browser: ['edge', 'chrome', 'firefox']
    runs-on: windows-latest
    timeout-minutes: 20
    needs: [install-apps]
    steps:
      - uses: actions/checkout@v2
      - name: Cache node modules
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
      - name: Run TestCafe    
        run: yarn run e2e    
        env:    
          BROWSER: ${{ matrix.browser }} 
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: reports-windows-${{ matrix.browser }}
          path: reports/

package.json に下記の記述を追加します。

{
  "scripts": {    
    "e2e": "node e2e/runner.js", 
    ...

e2e/runner.js に下記の記述を追加します。

このような流れです。 1. サーバー起動 2. HTTPリクエストが成功するまで待つ 3. テスト実行

const child_process = require('child_process')
const consola = require('consola')
const crossSpawn = require('cross-spawn')
const http = require("http")

const spawn = (command) => {
  const [c, ...args] = command.split(' ')
  return crossSpawn(c, args)
}

const serve = () => {
  const served = spawn('yarn run dev')
  served.stdout.on('data', (data) => {
    consola.log(`${data}`)
  })
  served.stderr.on('data', (data) => {
    consola.error(`${data}`)
  })
  served.on('close', (code) => {
    consola.info(`${code}`)
  })

  return served
}

const runE2E = (served) => {
  const command = `yarn run testcafe ${process.env.BROWSER}`
  const e2e = spawn(command)
  e2e.stdout.on('data', (data) => {
    consola.log(`${data}`)
  })
  e2e.stderr.on('data', (data) => {
    consola.error(`${data}`)
  })
  e2e.on('close', (code) => {
    served.kill()
    process.exit(code)
  })
}

const checkConnection = () => {
  return new Promise((resolve, reject) => {
    const helthCheckURL = 'http://localhost:3035'
    const requestTimeout = 1000
    const req = http
      .get(helthCheckURL, resolve)
      .on('error', reject);
    req.setTimeout(requestTimeout)
    req.on('timeout', () => {
      req.abort()
    })
  })
}

const main = () => {
  const run = async (served) => {
    try {
      await checkConnection()
      runE2E(served)
    } catch (_error) {
      const retryInterval = 1000
      setTimeout(run.bind(this, served), retryInterval)
    }
  }
  run(serve())
}

main()

これをGithub Actions上で実行すると、安定して成功します。

Mac: Safari

Mac上のSafariの実行も一工夫が必要になります。

公式にも普通にやったらGithub Actions上では動かないよ。 という記載があり、解決策もセットで書かれています。

Github Actions use the macOS Catalina 10.15 virtual environment with "System Integrity Protection" enabled as macos-latest. With this setting enabled, TestCafe requires screen recording permission, which cannot be obtained programmatically. For this reason, TestCafe is unable to run tests with GitHub Actions locally on macos-latest.

https://devexpress.github.io/testcafe/documentation/guides/continuous-integration/github-actions.html

公式のやり方を参考に、 e2e/run_safari.sh というファイルを作成します。

export HOSTNAME=localhost
export PORT1=1337
export PORT2=1338
yarn run testcafe remote --hostname ${HOSTNAME} --ports ${PORT1},${PORT2} &
pid=$!
sleep 10
open -a Safari http://${HOSTNAME}:${PORT1}/browser/connect
wait $pid

そして、それを実行させるために e2e/runner.js にも修正を加えます。

...
const runE2E = (served) => {
  let command
  switch (`${process.env.BROWSER}`) {
    case 'safari':
      command = `./e2e/run_safari.sh`
      break;
    default:
      command = `yarn run testcafe ${process.env.BROWSER}`
  }
  const e2e = spawn(command)
  e2e.stdout.on('data', (data) => {
    consola.log(`${data}`)
  })
  e2e.stderr.on('data', (data) => {
    consola.error(`${data}`)
  })
  e2e.on('close', (code) => {
    served.kill()
    process.exit(code)
  })
}
...

.github/workflows/test-e2e.yml はこのようになります。 windowsと記載のある部分をmacosに修正したくらいの変更です。

name: E2E Test

on: [push]

jobs:
  install-apps:
    name: install
    strategy:
      matrix:
        os: [macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v2
      - name: Cache node modules
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
      - name: yarn install
        run: yarn install --frozen-lockfile

  test-macos:
    name: Run tests safari on macos
    strategy:
      matrix:
        browser: ['safari']
    runs-on: macos-latest
    timeout-minutes: 20
    needs: [install-apps]
    steps:
      - uses: actions/checkout@v2
      - name: Cache node modules
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
      - name: Run TestCafe
        run: yarn run e2e
        env:
          BROWSER: ${{ matrix.browser }}
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: reports-macos-${{ matrix.browser }}
          path: reports/

とすると、このように成功します。

ただし、たまに下記のようなエラーが発生するので、調査が必要そうです。 ERROR NativeBinaryHasFailedError: The find-window process failed with the 1 exit code.

おわりに

お手軽に、、といいつつ、あんまりお手軽ではなかったです。

モバイルに関しては、次の記事で解説していきます。