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

はじめに

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

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

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

前回の記事の続きで、Github Actions上でTest Cafeを実行させます。

今回はモバイル端末の実行に関して説明していきます。

Github Actions上でTestCafeを実行する

ローカルでの実行について記載した記事では3パターン記しましたが、 Github Actions上で完結する手法としてBrowser Stack以外について書いていきます。

Android: Google Chrome Mobile

Emulatorの起動について

Github Actions上でEmulatorを起動させる必要があります。

AzureのDevOpsを参考にしていますが、このようなスクリプトで起動することができます。

#!/usr/bin/env bash

# Install AVD files
echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --install 'system-images;android-27;google_apis;x86'

# Create emulator
echo "no" | $ANDROID_HOME/tools/bin/avdmanager create avd -n xamarin_android_emulator -k 'system-images;android-27;google_apis;x86' --force

$ANDROID_HOME/emulator/emulator -list-avds

echo "Starting emulator"

# Start emulator in background
nohup $ANDROID_HOME/emulator/emulator -avd xamarin_android_emulator -no-snapshot > /dev/null 2>&1 &
$ANDROID_HOME/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82'

$ANDROID_HOME/platform-tools/adb devices

echo "Emulator started"

https://github.com/MicrosoftDocs/azure-devops-docs/blob/master/docs/pipelines/ecosystems/android.md#test-on-the-android-emulator

実行する

ローカルで動かした内容の2パターンでも、安定して動作しました。

  • Appium
  • TestCafeのテスト対象をremoteにする

「TestCafeのテスト対象をremoteにする」についてコードを載せていきます。

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

  e2e-mobile-chrome:
    name: Run tests mobile chrome on macos
    runs-on: macos-latest
    timeout-minutes: 20
    needs: [install-apps]
    env:
      emulator-name: 'android_emulator'
      system-image: 'system-images;android-30;google_apis;x86_64'
    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 Android Emulator
        run: |
          echo "Creating emulator"
          echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --install "${{ env.system-image }}"
          echo "no" | $ANDROID_HOME/tools/bin/avdmanager create avd -n "${{ env.emulator-name }}" -k "${{ env.system-image }}" --force
          $ANDROID_HOME/emulator/emulator -list-avds

          echo "Starting emulator"
          nohup $ANDROID_HOME/emulator/emulator -avd "${{ env.emulator-name }}" -no-snapshot > /dev/null 2>&1 &
          $ANDROID_HOME/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82'
          $ANDROID_HOME/platform-tools/adb devices

          # Chrome初回起動時の画面起動をなくす(Azureの例にはない
          $ANDROID_HOME/platform-tools/adb shell 'echo "chrome --disable-fre --no-default-browser-check --no-first-run" > /data/local/tmp/chrome-command-line' 

          echo "Emulator started"
      - name: Run TestCafe
        run: yarn run e2e
        env:
          BROWSER: mobile-chrome
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: reports-macos-mobile-chrome
          path: reports/

BROWSERを mobile-chrome として書いたので、 e2e/runner.jsの起動部分をこのように追記します。

const runE2E = (served) => {
  let command
  switch (`${process.env.BROWSER}`) {
    case 'safari':
      command = `./e2e/run_safari.sh`
      break;
    case 'mobile-chrome':
      command = `./e2e/run_mobile_chrome.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)
  })
}

そして、上記で指定しているスクリプトを追加します。

e2e/run_mobile_chrome.sh

export HOSTNAME=`/sbin/ifconfig en0 inet | grep 'inet ' | sed -e 's/^.*inet //' -e 's/ .*//'`
export PORT1=1337
export PORT2=1338
yarn run testcafe remote --hostname ${HOSTNAME} --ports ${PORT1},${PORT2} &
pid=$!
sleep 5
adb shell am start http://${HOSTNAME}:${PORT1}/browser/connect
wait $pid

これで動きました ○

ただし、Emulatorの作成・起動に6分くらいかかるので、作成したEmulatorはキャッシュできたら良いかなと思いました。

iPhone: Mobile Safari

Simulatorについて

こちらに記載のあるSimulatorは自由に扱うことができます。

https://github.com/actions/virtual-environments/blob/main/images/macos/macos-10.15-Readme.md

実行する

ローカルで動かした内容の2パターンで検証しましたが、どちらも安定的に動かすことができませんでした。

  • Appium
  • TestCafeのテスト対象をremoteにする

Appium

何度も試してみましたが、TestCafeの実行で下記のエラーが発生して失敗します。

ERROR Unable to establish one or more of the specified browser connections. This can be caused by network issues or remote device failure.

一つのエラーが発生するだけであれば調査をしやすいのですが、 Appium側のログにはエラーが出ていない場合もあり、エラーが出ている場合は下記のどれかが表示されていました。 (これらがTestCafe失敗の直接原因かも判断つかず)

  • [XCUITest] UnknownError: An unknown server-side error occurred while processing the command. Original error: Could not proxy command to the remote server. Original error: connect ECONNREFUSED 127.0.0.1:8100
  • [W3C] Encountered internal error running command: Error: Missing parameter: appIdKey
  • [W3C] Encountered internal error running command: Error: Could not navigate to webview! Err: Command 'lsof -aUc launchd_sim' exited with code 1

ログが出ていないということはタイムアウトか?という可能性もあるので、設定できるパラメータを色々試してみましたが、どれも解決には至りませんでした。

http://appium.io/docs/en/writing-running-appium/caps/ http://appium.io/docs/en/writing-running-appium/server-args/ https://github.com/appium/appium-xcuitest-driver

TestCafeのテスト対象をremoteにする

こちらも安定して実行することはできませんでした。。

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

  e2e-mobile-safari:
    name: Run tests mobile safari on macos
    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 iPhone Emulator
        run: nohup xcrun simctl boot "iPhone 12" > /dev/null 2>&1 &
      - name: Run TestCafe
        run: yarn run e2e
        env:
          BROWSER: mobile-safari
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: reports-macos-mobile-safari
          path: reports/

BROWSERを mobile-safari として書いたので、 e2e/runner.jsの起動部分をこのように追記します。

const runE2E = (served) => {
  let command
  switch (`${process.env.BROWSER}`) {
    case 'safari':
      command = `./e2e/run_safari.sh`
      break;
    case 'mobile-safari':
      command = `./e2e/run_mobile_safari.sh`
      break;
    case 'mobile-chrome':
      command = `./e2e/run_mobile_chrome.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)
  })
}

そして、上記で指定しているスクリプトを追加します。

e2e/run_mobile_safari.sh

export HOSTNAME=localhost
export PORT1=1337
export PORT2=1338
yarn run testcafe remote --hostname ${HOSTNAME} --ports ${PORT1},${PORT2} &
pid=$!

sleep 30
xcrun simctl openurl booted http://${HOSTNAME}:${PORT1}/browser/connect
wait $pid

こちらも色々試しましたが、成功率は3割くらいでした。

このようなエラーが表示されます。 Github Actions上のMacのSafari実行時にも起こるエラーなので、根本原因は一緒かもしれません。

...
[log] Connecting 1 remote browser(s)...
Navigate to the following URL from each remote browser.

[log] Connect URL: http://localhost:1337/browser/connect

[log] CONNECTED Safari 14.0.3 / iOS 14.4

[log] ERROR NativeBinaryHasFailedError: The find-window process failed with the 1 exit code.
Process output:
LSOpenURLsWithRole() failed for the application /Users/runner/work/hogehoge/hogehoge/node_modules/testcafe-browser-tools/bin/mac/TestCafe Browser Tools.app/Contents/MacOS/testcafe-browser-tools with error -10810.
    at ChildProcess.<anonymous> (/Users/runner/work/hogehoge/hogehoge/node_modules/testcafe-browser-tools/src/utils/exec.js:73:24)
    at ChildProcess.emit (events.js:315:20)
    at ChildProcess.EventEmitter.emit (domain.js:467:12)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:277:12)
    at Process.callbackTrampoline (internal/async_hooks.js:131:14)

Type "testcafe -h" for help.

Error:  error Command failed with exit code 1.
[log] info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

テスト実行のトリガー

今回は push のタイミングでGithub Actionsを起動させてましたが、 workflow dispatch で任意のタイミングで起動させることもできるので、Slackから特定コマンドで実行させるとか、そういう連携もできます。

https://docs.github.com/ja/actions/reference/events-that-trigger-workflows

おわりに

モバイルの実行は不安定ということもあり、素直にBrowserStack使ったほうがベターかなという印象でした。

何にせよ、こんなことまで出来るGithub Actionsスゴイ。。ってなりました。

おまけ

これを試してる時に得た知識を書き残しておきます。

Github Actionsの実行は、無料枠で収まるように注意する

プランによりけりですが、無料枠で収まるように実行した時間はチェックする必要があります。

https://github.com/settings/billing

GitHub Enterprise Cloud なら5万分だし余裕じゃん、とか思うかも知れませんが、 Macで何も考えずに実行しまくると余裕で上限にいってしまいます。

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

リセットのタイミングは「毎月1日の17時」でした。

JOBのタイムアウトを指定する

デフォルトのタイムアウトは各ジョブで360分です。 意図せず実行が長くなることもあるので、timeout-minutes は指定した方が安全です。 https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions

Github Actionsのキャッシュについて

  • キャッシュ上限: 1つのリポジトリ内のすべてのキャッシュで5GBまで
    • 上限を超えた場合、合計サイズが5GB以下になるまでキャッシュを退去させる
  • 削除タイミング: 7日間以上アクセスされないとキャッシュが削除される

https://docs.github.com/ja/actions/guides/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy

Github ActionsのJOB間でのファイル共有、ファイル出力について

artifactsという機能で行えます。 設定で変えることもできますが、デフォルトでは90日間保存されます。

https://docs.github.com/ja/actions/guides/storing-workflow-data-as-artifacts