CircleCi・Github ActionsそれぞれでAndroidアプリのE2Eテストを自動化する

## はじめに
1から簡単なAndroidアプリを作成し、それに対するE2EテストをAppiumで自動化しCI環境で実行できるようにする所までをやってみたのでその備忘録を残しておく。

※本記事はwebdriverioを用いているが、あえて最小で構築しているので`@wdio/cli`は利用していない。

構築したアプリ・e2e自動テストのコード・CIパイプラインのコードは以下のGitHubにある。

https://github.com/yuta-katayama-23/mobile-app-e2e-test

## 内容
セクションは大きく3つで、

- アプリの実装について（[Androidアプリを作成する](#androidアプリを作成する)）
- e2eテスト自動化のソースコードの実装について（[AppiumによるE2Eテスト](#appiumによるe2eテスト)）
- CI上での自動テスト実行について（[CI](#ci)）

に分かれている。

## Androidアプリを作成する
### アプリの実装
[Androidアプリのdeveloper向けのサイト](https://developer.android.com/)にある公式チュートリアルがステップバイステップで丁寧＆日本語字幕・解説があるので、これでアプリを作成するのがおススメ。

[Kotlin での Android の基礎](https://developer.android.com/courses/android-basics-kotlin/course?hl=ja)
※見た目は日本語モードでも英語になっているが、コンテンツの中身・字幕は日本語がある

※上記の**Kotlin での Android の基礎**は、
　https://developer.android.com/training/basics/firstapp?hl=ja#where-to-go-from-here から飛べるリンクのページ

#### AVD（エミュレータ）の作成
アプリを実際にPC上で起動しその動きを確認するためにはAVD（エミュレータ）が必要になるので[仮想デバイスを作成して管理する](https://developer.android.com/studio/run/managing-avds?hl=ja)を参照して作成する。

※ちなみに、このAVDはAppiumでE2Eテストを実行する時にも必要になる。

#### AVD（エミュレータ）の起動方法
Android Studioから起動するには以下の図の〇で作成したAVD名を選択し、▶をクリックすればいい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/a147f53b-91c5-e329-c9cf-401e829f539e.png)

ただ、このやり方だとAVDの起動時にイチイチAndroid Studioを立ち上げる必要があり面倒。
そこで`emulator -list-avds`コマンドで作成済みのAVD一覧を表示させ、その`emulator {AVD名}`コマンドを実行するとAndroid Studioから毎回起動しなくてもAVDを起動できる。

```
C:\Users\user>emulator -list-avds
Pixel_3_XL_API_28

C:\Users\user>emulator @Pixel_3_XL_API_28
```

### E2Eテストの実装対象になるアプリ一覧
#### 実際に作成したアプリ①　Happy Birthday Card App
単にメッセージが表示されているだけのアプリで、ソースコード全体は[ここ](https://github.com/yuta-katayama-23/mobile-app-e2e-test/tree/happybirthday-app)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/8b782d89-b145-0b17-027e-cb29b240366f.png)

## AppiumによるE2Eテスト
モバイルアプリのE2Eテスト自動化で定番の[Appium](https://appium.io/)を使ってE2Eテストを実装した。
E2Eテストを実装する上でやった事としては以下。

- ローカル環境構築（CIについては[CI](#ci)の項を参照）
- webdriverioの導入・テスト実装
- mochaの導入
- ロケータ指定する部分はutilityに
- webdriverioのlogをconsole上に出力させない

実際にE2Eテストを実装する上でどのようにロケータ情報などを調べるのか？については以下の項を参照。

- debbugerでのロケータ取得方法

### ローカル環境構築
[`appium-doctor`](https://github.com/appium/appium-doctor)でAndroidやiOSのアプリのテストのためのAppiumを実行する環境があるかを確認できる。
私の環境では以下のような結果だったのでJAVA_HOME・ANDROID_HOME（Java SDK, Android SDKのパス）の設定を行ったのでその時のlogを以下に書き残す（Windows）。

```shell
> appium-doctor

info AppiumDoctor Appium Doctor v.1.16.0
info AppiumDoctor ### Diagnostic for necessary dependencies starting ###
info AppiumDoctor  ✔ The Node.js binary was found at: C:\Program Files\nodejs\node.EXE
info AppiumDoctor  ✔ Node version is 14.16.0
WARN AppiumDoctor  ✖ ANDROID_HOME environment variable is NOT set!
WARN AppiumDoctor  ✖ JAVA_HOME environment variable is NOT set!
WARN AppiumDoctor  ✖ adb, android, emulator could not be found because ANDROID_HOME or ANDROID_SDK_ROOT is NOT set!
WARN AppiumDoctor  ✖ Cannot check %JAVA_HOME% requirements since the environment variable itself is not set
info AppiumDoctor ### Diagnostic for necessary dependencies completed, 4 fixes needed. ###
```

#### JAVA_HOMEの設定
https://adoptopenjdk.net/ から openjdk11 をインストールする。
※インストールの際、カスタムセットアップの画面で`Set JAVA_HOME variable`がデフォルトだと`× インストールしない`になっているので、`ローカルハードドライブにインストール`に変更しないとパスが通らない。

インストール完了後、`javac --version`とコマンドプロンプトで実行しバージョンが表示されれば OK。
（バージョンが表示されない時は再起動してJAVA_HOMEのパスがwindowsに通るようにする。）

#### ANDROID_HOMEの設定
Androidのアプリを作成するためにAndroid Studioをインストールすると、必要なAndroid SDK（＋ SDK Tools）はインストール済みなはずなので、あとはそのSDKがある場所をWindowsの環境変数に設定してパスを通せばいい。
ただ、環境変数の登録にあたっては順番が大事なので注意。

私の環境では、`C:\Users\user\AppData\Local\Android\Sdk`に SDK があったのでそれをそれぞれ、

- ANDROID_HOME<br>C:\Users\user\AppData\Local\Android\Sdk
- Pathに以下の順でそれぞれ追加
  - %ANDROID_HOME%emulator
  - %ANDROID_HOME%tools
  - %ANDROID_HOME%platform-tools

を環境変数に追加した。

最終的に再度appium-doctorを実行して以下のように`necessary dependencies`が全て✔になっていればOK。

```shell
info AppiumDoctor Appium Doctor v.1.16.0
info AppiumDoctor ### Diagnostic for necessary dependencies starting ###
info AppiumDoctor  ✔ The Node.js binary was found at: C:\Program Files\nodejs\node.EXE
info AppiumDoctor  ✔ Node version is 14.16.0
info AppiumDoctor  ✔ ANDROID_HOME is set to: C:\Users\user\AppData\Local\Android\Sdk
info AppiumDoctor  ✔ JAVA_HOME is set to: C:\Program Files\AdoptOpenJDK\jdk-11.0.11.9-hotspot\
info AppiumDoctor    Checking adb, android, emulator
info AppiumDoctor      'adb' is in C:\Users\user\AppData\Local\Android\Sdk\platform-tools\adb.exe
info AppiumDoctor      'android' is in C:\Users\user\AppData\Local\Android\Sdk\tools\android.bat
info AppiumDoctor      'emulator' is in C:\Users\user\AppData\Local\Android\Sdk\emulator\emulator.exe
info AppiumDoctor  ✔ adb, android, emulator exist: C:\Users\user\AppData\Local\Android\Sdk
info AppiumDoctor  ✔ 'bin' subfolder exists under 'C:\Program Files\AdoptOpenJDK\jdk-11.0.11.9-hotspot\'
info AppiumDoctor ### Diagnostic for necessary dependencies completed, no fix needed. ###
```

### webdriverioの導入・テスト実装
今回はappiumでの自動化なので、appium serverとの通信を行う事になのでセッションを作る必要があるが、Appiumの[Getting Started](https://appium.io/docs/en/about-appium/getting-started/)に紹介されている[やり方](https://webdriver.io/docs/setuptypes/#standalone-mode)に沿って以下のように実装する。
（以下は単にセッションを繋いで切るだけのソースコード。`remote()`で作成したセッションに対してhttps://webdriver.io/docs/api/webdriver に書かれているメソッドを使って実際にアプリを操作する。）

`capabilities`については[ここ](https://appium.io/docs/en/writing-running-appium/caps/index.html)を参照。

```js
const wdio = require("webdriverio");

const opts = {
    path: '/wd/hub',
    port: 4723,
    capabilities: {
        platformName: "Android",
        platformVersion: "9", // ここはandroidのemulatorのversion
        deviceName: "Android Emulator",
        app: `${__dirname.replace("__e2e__", "")}app/build/outputs/apk/debug/app-debug.apk`,  // Androidのアプリ本体
        automationName: "UiAutomator2"
    }
};

async function main() {
    const client = await wdio.remote(opts);
    await client.deleteSession();
}

main();
```

### mochaの導入
単純に以下のようにwebdriverioで操作をして[`expect.js`](https://github.com/Automattic/expect.js)でassertionをしてもlog上はappium clientのlogが出るだけ結果が分かりにくいので[mocha](https://github.com/mochajs/mocha)を導入した。

```js:index.js
// 省略
async function main() {
  const client = await wdio.remote(opts);

  const field = await client.$('//*[@resource-id="com.example.happybirthday:id/textView"]');
  const value = await field.getText();
  expect(value).to.equal("Happy Birthday, Sam!");

  await client.deleteSession();
}

main();
```
```
node index.js
```

mochaの導入にあたって見たページは以下。

- [INSTALLATION](https://mochajs.org/#installation)
- [GETTING STARTED](https://mochajs.org/#getting-started)
- [COMMAND-LINE USAGE](https://mochajs.org/#command-line-usage)
- [REPORTERS](https://mochajs.org/#reporters)

mochaで書いたソースコードは以下。

```js
const wdio = require("webdriverio");
const expect = require("expect.js");
const by = require("../wedio/locator-helper");
const opts = require("../wedio/config");

describe('Create session and delete session', () => {
    let client;

    before(async () => {
        client = await wdio.remote(opts);
    });

    describe('Happy Birthday App Test', () => {
        it('check title text', async () => {
            const field = await client.$("//android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[1]/android.view.ViewGroup/android.widget.TextView");
            const value = await field.getText();
            expect(value).to.equal("Happy Birthday");
        });

        it('check message text', async () => {
            const field = await client.$(by.resourceId("com.example.happybirthday:id/textView"));
            const value = await field.getText();
            expect(value).to.equal("Happy Birthday, Sam!");
        });

        it('check from text', async () => {
            const field = await client.$(by.resourceId("com.example.happybirthday:id/textView2"));
            const value = await field.getText();
            expect(value).to.equal("From Emma.");
        });
    })

    after(async () => {
        await client.deleteSession();
    });

})
```

`const by = require("../wedio/locator-helper");`や`const opts = require("../wedio/config");`のソースコードは[GitHub](https://github.com/yuta-katayama-23/mobile-app-e2e-test/tree/happybirthday-app/__e2e__/wedio)から確認できる。

### ロケータ指定する部分はutilityに
webとかであれば`findElement()`などのメソッドをわざわざ呼び出さずとも[selectors](https://webdriver.io/docs/selectors)に書かれているような方法で簡単に要素を探し出せるが、Androidなどのモバイルだとそうはいかず、何通りかの方法で要素をしているする事になる。
※Android独自の要素指定方法である[UiSelector](https://developer.android.com/reference/androidx/test/uiautomator/UiSelector) を用いて要素を探す時は[Android UiAutomator](https://webdriver.io/docs/selectors#android-uiautomator)に書かれている方法が使える。

- resource-id
- xpath
- UiSelector

こういったものをソースコード上に書いてもいいがutilityとして別に切り出して用意しておくと便利。
具体的には以下のような感じ。

```js
module.exports = {
    /**
     * リソースIDを指定するセレクタ
     * @param {string} id resourceId-id
     * @returns xpath
     */
    resourceId(id) {
        return `//*[@resource-id="${id}"]`;
    },

    /**
     * クラス名を指定するセレクタ
     * @param {string} className 
     * @returns UiSelector
     */
    className(className) {
        return `android=new UiSelector().className("${className}")`;
    }
}
```

### webdriverioのlogをconsole上に出力させない
何も設定をしないと以下のようにappium clientのlogとmochaのlogがごっちゃになって何が何だか分からなくなる。

```
> mocha --timeout 30s

  Happy Birthday App Test
2021-07-30T07:32:27.849Z INFO webdriver: Initiate new session using the WebDriver protocol
2021-07-30T07:32:27.981Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session
2021-07-30T07:32:27.982Z INFO webdriver: DATA {  省略
2021-07-30T07:32:40.013Z INFO webdriver: COMMAND findElement("xpath", "//android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[1]/android.view.ViewGroup/android.widget.TextView")
2021-07-30T07:32:40.015Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/27d30517-4d1e-4128-bb9e-733c5dd774d3/element
2021-07-30T07:32:40.015Z INFO webdriver: DATA {
  using: 'xpath',
  value: '//android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[1]/android.view.ViewGroup/android.widget.TextView'
}
2021-07-30T07:32:40.255Z INFO webdriver: RESULT {
  'element-6066-11e4-a52e-4f735466cecf': 'f65a9d48-fff2-40b1-8301-5f11fa307990',
  ELEMENT: 'f65a9d48-fff2-40b1-8301-5f11fa307990'
}
2021-07-30T07:32:40.269Z INFO webdriver: COMMAND getElementText("f65a9d48-fff2-40b1-8301-5f11fa307990")
2021-07-30T07:32:40.269Z INFO webdriver: [GET] http://127.0.0.1:4723/wd/hub/session/27d30517-4d1e-4128-bb9e-733c5dd774d3/element/f65a9d48-fff2-40b1-8301-5f11fa307990/text
2021-07-30T07:32:40.303Z INFO webdriver: RESULT Happy Birthday
    ✔ check title text (294ms)
2021-07-30T07:32:40.308Z INFO webdriver: COMMAND findElement("xpath", "//*[@resource-  省略
2021-07-30T07:32:40.309Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/27d30517-4d1e-4128-bb9e-733c5dd774d3/element
2021-07-30T07:32:40.309Z INFO webdriver: DATA  省略
2021-07-30T07:32:40.379Z INFO webdriver: RESULT  省略
2021-07-30T07:32:40.387Z INFO webdriver: COMMAND getElementText("0152bc52-dff5-4638-9979-e4689d0f71eb")
2021-07-30T07:32:40.387Z INFO webdriver: [GET] http://127.0.0.1:4723/wd/hub/session/27d30517-4d1e-4128-bb9e-733c5dd774d3/element/0152bc52-dff5-4638-9979-e4689d0f71eb/text
2021-07-30T07:32:40.413Z INFO webdriver: RESULT Happy Birthday, Sam!
    ✔ check message text (109ms)
2021-07-30T07:32:40.418Z INFO webdriver: COMMAND findElement("xpath", "//*[@resource-  省略
21-07-30T07:32:40.482Z INFO webdriver: RESULT  省略
2021-07-30T07:32:40.488Z INFO webdriver: COMMAND getElementText("17f89d91-f14e-47af-aebd-d365a9dfb67d")
2021-07-30T07:32:40.489Z INFO webdriver: [GET] http://127.0.0.1:4723/wd/hub/session/27d30517-4d1e-4128-bb9e-733c5dd774d3/element/17f89d91-f14e-47af-aebd-d365a9dfb67d/text
2021-07-30T07:32:40.511Z INFO webdriver: RESULT From Emma.
    ✔ check from text (94ms)
2021-07-30T07:32:40.512Z INFO webdriver: COMMAND deleteSession()
2021-07-30T07:32:40.513Z INFO webdriver: [DELETE] http://127.0.0.1:4723/wd/hub/session/27d30517-4d1e-4128-bb9e-733c5dd774d3

  3 passing (14s)
```

そのためwebdriverioのoptionに`outputDir`キーを追加し、

```
outputDir: `${__dirname.replace(/__e2e__(\\|\/)wedio/, "")}__e2e__/`
```

のような記載してappium clientのlogは別の所に出力させるのがいいと思われる。

### debbugerでのロケータ取得方法
e2eテストを実装していくにあたっては、要素を取得しそれに対して何らかのアクションを実行するがその要素を取得するためのロケータ情報は[Appium Desktop](https://github.com/appium/appium-desktop/releases/tag/v1.21.0)で行う。
ロケータ情報を取得する時は、appium clientからAVD上にアプリを起動し、その状態で一時的にStopさせる必要があるのでnode の`inspect`オプション × `debugger`を使う。

[^inspect]: https://mochajs.org/#-inspect-inspect-brk-inspect
ソースコード上に以下のように`debugger`を置き、`node --inspect hoge.js`のようにテストを起動する。
（今回はmochaだが要領は同じで、`debugger`を置き、`mocha inspect --no-timeout`[^inspect]でテストを実行すればいい。）

```js
describe('Create session and delete session', () => {
    let client;

    before(async () => {
        client = await wdio.remote(opts);
    });

    it('debugger', async () => {
        debugger;
    });

    after(async () => {
        await client.deleteSession();
    });

})
```

以下実際の流れ。

1. appium serverとAVDを起動する
1. テストを実行すると、Nodeのinspectの機能でコマンドラインで止まるのでそこで一旦放置
1. Appium Desktopを起動する
1. `File > New Session Window... > Attach to Session...タブ > Attach to Sessionボタン`で以下のような画面が表示されるのでここからロケータを取得する![appium.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/9560b703-cd9e-940f-7ecb-53d8f973d6aa.png)

※この時のポイントとしては、

- newCommandTimeout: 300 など長い時間に設定する<br>デフォルトでは 60（60 秒） になっているので、デバッグポイントで一時停止させた後、何も操作をしない（client から server にコマンドを送信しない）と 60 秒で session が切れてしまうため<br>※newCommandTimeoutは`capabilities`のキーの1つ<br>https://appium.io/docs/en/writing-running-appium/caps/
- --no-timeout に設定する<br>デフォルトでは 2s になっているので、`mocha inspect --no-timeout`のように timeout しないようにしてからテスト実行しないと、appium desktop でロケータ情報などを取得している最中にデバッグが終了してしまう

のようにする事。

###テスト対象アプリ
#### テスト対象アプリ①　Happy Birthday Card App
アプリとしては特にユーザが操作する事は全くできないアプリで、画面にテキストが表示されているもの。
そのためテスト内容としてはアプリに表示されているテキストが期待通りか？をassertionするテストを作成。
ソースコードは以下。

https://github.com/yuta-katayama-23/mobile-app-e2e-test/blob/happybirthday-app/__e2e__/test/index.js

## CI
ローカル環境でE2Eテストが実行できているだけではあまり意味がないため、CI環境で実行できるようにCIのパイプラインを構築した。
せっかくなので、

- CircleCi
- GitHub Actions

の2つのCICDプラットフォームでE2Eテスト自動化のパイプラインを構築した。

### CircleCi
CircleCiの場合は[公式ページ](https://circleci.com/docs/2.0/android-machine-image/#no-orb-example)にある程度ヒントになるyamlがあるのでそれを参考にすればいい。
使うコンテナは[Using Android Images with the Machine Executor](https://circleci.com/docs/2.0/android-machine-image/)という便利なコンテナが標準で用意されているのでそれを使う。これを使うとAndroidアプリのbuildを行ったり、emulatorを作成・起動するのが簡単にできる。
（GitHub：https://github.com/CircleCI-Public/android-image-preview-docs ）

実際に作成したパイプラインのソースコードは以下。

https://github.com/yuta-katayama-23/mobile-app-e2e-test/blob/happybirthday-app/.circleci/config.yml

### GitHub Actions
GitHub Actionsの場合は、コンテナの選択を間違えると詰るので注意。大体CIを構築する時は`ubuntu-latest`を使うと思うがこれだとAndroidのemulatorが起動できない。
そのためGitHub Actionsでは、**`macos-latest`**を使うようにする。
詳細は[ubuntu-latestでエラー『emulator: ERROR: x86 emulation currently requires hardware acceleration!』](#ubuntu-latest-でエラーemulator-error-x86-emulation-currently-requires-hardware-acceleration)を使うを参照。

実際に作成したパイプラインのソースコードは以下。

https://github.com/yuta-katayama-23/mobile-app-e2e-test/blob/happybirthday-app/.github/workflows/android-app-build-and-e2etest.yml

※GitHub Actionsでのe2eテスト自動化は少し手間取った箇所があったのでそれを以下の[トラブルシューティング](#トラブルシューティング)に書き残しておく。

### トラブルシューティング
#### sdkmanager, avdmanager コマンドの実行
`sdkmanager`コマンドを実行しようと数するもエラー（[GitHub Actions の JobURL はこれ](job：https://github.com/yuta-katayama-23/MobileAppE2ETest/runs/3188836735?check_suite_focus=true)）

```bash
sdkmanager "system-images;android-28;default;x86"
avdmanager --verbose create avd -n test -k "system-images;android-28;default;x86"

/home/runner/work/_temp/af62e0b9-d807-4eb2-87a6-f1e203106b6c.sh: line 2: sdkmanager: command not found
```

単純にデフォルトでパスが通っていないようなので、どこに`sdkmanager`がいるかを探す必要がある。
探すと`sdkmanager`は`android_sdk/cmdline-tools/version/bin/`にあるので、

```
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager
```

のように実行するればよい。
参考：[Android SDK コマンドライン ツール](https://developer.android.com/studio/command-line?hl=ja#tools-sdk)

#### ライセンスへの同意
`sdkmanager`コマンドを実行すると Android SDK のライセンス同意が表示され、それに対し標準入力を行わないとエラーになる。
これは

```
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "$SYSTEM_IMAGES"
```

のようにする事でコマンドからの質問に応答できる。

#### emulator コマンドの実行
`emulator`は`android_sdk/emulator/`にあるので

```
$ANDROID_HOME/emulator/emulator -avd test -verbose -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
```

のように実行する。
参考：[Android エミュレータ](https://developer.android.com/studio/command-line?hl=ja#tools-emulator)

※ちなみに、emulator 起動時に引数に色々オプションを付けられるがそれぞれ以下の通り。

| コマンドライン オプション | 説明 |
| ----------------- | ---------------------------------------------------------------------- |
| -avd _avd name_ | 起動する avd の名前を指定する |
| -verbose | エミュレータの初期化メッセージをターミナル ウィンドウに出力させる |
| -no-window | エミュレータのグラフィカル ウィンドウ表示を無効にする |
| -gpu _mode_ | グラフィック アクセラレーション タイプを*mode*に指定する |
| -no-snapshot | スナップショットの復元後すぐに AVD クロック時刻の修正を試みないようにする |
| -noaudio | 仮想デバイスのオーディオ サポートを無効にする |
| -no-boot-anim | 起動を高速化するため、エミュレータ起動中の起動アニメーションを無効にする |

参考：[コマンドラインからのエミュレータの起動](https://developer.android.com/studio/run/emulator-commandline)
参考：[コマンドラインからグラフィック アクセラレーションを設定する](https://developer.android.com/studio/run/emulator-acceleration?hl=ja#command-gpu)

#### emulatorがbootするまで待機する
emulator が起動するまでには少し時間がかかり、起動が完了してからテスト実行しないとテストが失敗する。
のでemulatorが起動完了する（boot completed）まで待つ必要がある。

emulator が起動完了すると以下のような log が出るが、

```
emulator: INFO: boot completed
```

これが出るまで待つには、

```bash
adb shell getprop init.svc.bootanim
```

というコマンドを利用すればいい。
実際の実装としては以下のようになる。

```bash
while [ "`adb shell getprop sys.boot_completed | tr -d '\r'`" != "1" ] ; do sleep 10 && echo "wait for boot..."; done
```

参考：[Detect when Android emulator is fully booted](https://stackoverflow.com/questions/3634041/detect-when-android-emulator-is-fully-booted)
参考：[【 tr 】コマンド――テキストファイルの文字を置換する／削除する](https://www.atmarkit.co.jp/ait/articles/1610/03/news017.html)

#### Background実行のjobのlogをArtifactに保存
CircleCi では `background: true` で backgroud 実行させてそのlogも見れるが、GitHub Actionsではbackgroundで実行させる事はできてもそのlogがGitHub Actionsのconsoleに出力されない。
ex) https://github.com/yuta-katayama-23/MobileAppE2ETest/runs/3191394079?check_suite_focus=true

そのためappium server、AVDのlogそれぞれをArtifactに保存するように実装した。

```
      - name: Start android emulator
        # https://developer.android.com/studio/run/emulator-commandline
        run: |
          mkdir ~/log
          $ANDROID_HOME/emulator/emulator -avd test -verbose -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim >> ~/log/emulator.log &

      # 省略

      - name: Start appium server
        run: |
          cd __e2e__
          npm run s:appium >> ~/log/appium.log &

      # 省略

      - name: Archive emulator and appium server logs
        if: always()
        uses: actions/upload-artifact@v2
        with:
          name: log
          path: ~/log
```

#### ubuntu-latest でエラー『emulator: ERROR: x86 emulation currently requires hardware acceleration!』
（前置き：ここには`ubuntu-latest`でCIを構築しようとして失敗した時の作業logも細かく書いてある。）

GitHub Actions も CircleCi と考え方は同じで、基本的にはローカルでエミュレータ起動・Appium Server の起動を行い、その後テスト実行を行う手順と同じ事をコマンドで行えば GitHub Actions 上でも E2Eテストが実行できる。

GitHub Actions の Runner `ubuntu-latest` に[元々インストール済み](https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md)のものを見ると、Android のアプリ build・E2E テストに必要なものはほぼインストール済みで、インストールする必要があるのは appium だけ。
※ちなみに、GitHub Actions の Runner 一覧は[ここ](https://github.com/actions/virtual-environments#available-environments)。

使うコンテナを`ubuntu-latest`に決め、パイプライン実装して実行すると、以下のようなlogが出てエラーに（jobのURLは[これ](https://github.com/yuta-katayama-23/MobileAppE2ETest/runs/3189440630?check_suite_focus=true)。）
logの内容から単純にKVMが必要そうと分かり、logに出ているリンクにアクセス。

```
emulator: CPU Acceleration: DISABLED
emulator: CPU Acceleration status: KVM requires a CPU that supports vmx or svm
emulator: ERROR: x86 emulation currently requires hardware acceleration!
CPU acceleration status: KVM requires a CPU that supports vmx or svm
More info on configuring VM acceleration on Linux:
https://developer.android.com/studio/run/emulator-acceleration#vm-linux
General information on acceleration: https://developer.android.com/studio/run/emulator-acceleration.
```

Android 公式のドキュメントに沿って Ubuntu KVM のインストールを行っていく
> Linux ベースのシステムは、[KVM ソフトウェア パッケージ](https://www.linux-kvm.org/page/Main_Page)を介して VM アクセラレーションをサポートしています。 Linux システムに KVM をインストールする手順を行い、KVM が有効になっていることを確認します。Ubuntu システムについては、[Ubuntu KVM のインストール](https://help.ubuntu.com/community/KVM/Installation)の説明をご覧ください。

[Ubuntu KVM のインストール](https://help.ubuntu.com/community/KVM/Installation)のサイトに従ってやっていく（GitHub ActionsのRunner上で）。

```
sudo apt-get install cpu-checker
sudo /usr/sbin/kvm-ok
```

でチェックすると、、、

```
INFO: Your CPU does not support KVM extensions
KVM acceleration can NOT be used
```

と言われ、使えないの・・・となるが、以下のような注意書きがあり、**今**使える/使えないを示してるだけなので、install すれば OK そうと考える。
> NOTE: You may see a message like "KVM acceleration can/can NOT be used". This is misleading and only means if KVM is _currently_ available (i.e. "turned on"), _not_ if it is supported.（「KVM アクセラレーションを使用できます/使用できません」のようなメッセージが表示される場合があります。 これは誤解を招く可能性があり、KVM が*現在*利用可能（つまり「オン」）である場合にのみ意味し、サポートされている場合はそうではありません。）

ubuntu-latest は`Cosmic (18.10) or later`[^ubuntu]なので、

```
sudo apt-get install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils
```

を実行すればいい
[^ubuntu]: [Ubuntu のバージョン履歴](https://ja.wikipedia.org/wiki/Ubuntu%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E5%B1%A5%E6%AD%B4)

その後、`Add Users to Groups`を実行して`virsh list --all`でインストールが成功したか？を確認するもエラーが変わらず・・・

GitHub Actions の Marketplace にある[GitHub Action - Android Emulator Runner](https://github.com/marketplace/actions/android-emulator-runner)を見ると、
> The old ARM-based emulators were slow and are no longer supported by Google. The modern Intel Atom (x86 and x86_64) emulators require hardware acceleration (HAXM on Mac & Windows, QEMU on Linux) from the host to run fast. This presents a challenge on CI as to be able to run hardware accelerated emulators within a docker container, KVM must be supported by the host VM which isn't the case for cloud-based CI providers due to infrastructural limits.（古い ARM ベースのエミュレーターは低速であり、Google ではサポートされなくなりました。 最新の IntelAtom（x86 および x86_64）エミュレーターは、高速で実行するために、ホストからのハードウェアアクセラレーション（Mac および Windows では HAXM、Linux では QEMU）を必要とします。これは、Docker コンテナー内でハードウェアアクセラレーションエミュレーターを実行できるようにするという CI の課題を提示します。）
> The macOS VM provided by GitHub Actions has HAXM installed so we are able to create a new AVD instance, launch an emulator with hardware acceleration, and run our Android tests directly on the VM. You can also achieve this on a self-hosted Linux runner, but it will need to be on a compatible instance that allows you to enable KVM - for example AWS EC2 Bare Metal instances.（GitHubActions が提供する macOSVM には HAXM がインストールされているため、新しい AVD インスタンスを作成し、ハードウェアアクセラレーションを使用してエミュレーターを起動し、VM で Android テストを直接実行できます。 セルフホストの Linux ランナーでもこれを実現できますが、KVM を有効にできる互換性のあるインスタンス（AWS EC2 ベアメタルインスタンスなど）で実行する必要があります。）
> It is recommended to run this action on a macOS VM, e.g. macos-latest or macos-10.15 to take advantage of hardware accleration support provided by HAXM.（このアクションは、macOSVM で実行することをお勧めします。 macos-latest または macos-10.15 は、HAXM が提供するハードウェアアクセラレーションサポートを利用します。）

とか書いてあり、GitHub Actions 標準の`ubuntu-latest`でやるのは不可能である事が分かり方針転換で`macos-latest`に変更した。

[macOS で VM アクセラレーションを設定する](https://developer.android.com/studio/run/emulator-acceleration#vm-mac)に従って

```
kextstat | grep intel
```

で Intel HAXM カーネル拡張機能が使えるか？を確認すると、

```
121 0 0xffffff7f82c2f000 0x28000 0x28000 com.intel.kext.intelhaxm (7.6.5) 9C4BE7CB-745A-326F-81FF-6D7D61B68F87 <8 6 5 3 1>
```

と表示されたので OKそう。
その後emulator起動をしてみるとうまくいった。

## 参考文献
- https://swet.dena.com/entry/2018/10/10/180000