Remote TestKitで複数機種のテスト自動化やってみた

クラウド上で様々なスマホ/タブレット端末をレンタルし実機検証可能な「[Remote TestKit](https://appkitbox.com/testkit/)」を使って、アプリのテストを自動化してみました。
「様々な端末で検証可能」というメリット活かし、1つのテストコードで複数の機種を使ってテストを実行する手順を説明します。

この記事は[Qiita Engineer Festa 2022](https://qiita.com/official-campaigns/engineer-festa/2022)の記事投稿キャンペーンに参加しています。

# 3行でまとめると
 - Remote TestKitではクラウド上でスマホをレンタルし、ローカルPC(Windows/Mac)上で操作できます。
 - 様々な機種をレンタルでき、新発売の機種の導入も早いです。
 - Appiumを利用すると、端末へのapkインストール、テスト実行、結果の表示までを自動化できます。

# Remote TestKit
## Remote TestKitとは
Remote TestKitは様々なスマホ/タブレット端末（iPhone/iPadなど、Android）をクラウド上でレンタルし、実機を操作できます。

ソフトウェア版とブラウザ版があり、いずれも実機の画面を表示して各種操作を行えます。前者の方が多機能で、その他のツールとの連携もできます。

開発中（アプリストア未登録）のアプリを端末にインストールすることもできます。

## インストール
Remote Testkit ソフトウェア版はWindowsとMacに対応しています。[ダウンロード](https://appkitbox.com/testkit/download/)ページからインストーラーをダウンロードして実行すれば、簡単にインストールできます。

## 使い方
画面の上半分は自分がレンタル中の端末、下半分にはクラウド上にある端末が表示されています。後者は、OSバージョンや機種名などでフィルターや並び替えもできます。
端末をレンタルするには右下の「レンタル」ボタンをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20639/89da8c9f-8406-a0aa-a475-b9455acf71cb.png)

## 使うメリット
### 様々な機種を利用できる
スマホ（iPhone/Android）やタブレット（iPad/Android）700機種以上を利用可能です。利用可能な端末は[端末一覧](https://appkitbox.com/testkit/devicelist/)から検索できます。

### 最新機種の導入も早い
docomo、au、Softbankの新発売機種については原則2週間以内に利用できるそうです。2022年7月上旬に確認したところ、6月下旬に発売された機種が利用できるようでした。


# Appium
## Appiumとは
Appiumはモバイルアプリ用のUIテスト自動化ツールです。
クロスプラットフォームに対応しているため、iOS/Androidアプリを1つのコードでテストできます。ブラウザ操作を自動化できるSeleniumのアプリ版です。

テストはAppiumサーバーに対して、HTTPリクエストを投げてアプリの操作を指示することになります。HTTPリクエストを投げればよいため、テストコードは様々な言語で書くことができます。

## インストール
AppiumサーバーをローカルPCにインストールします。Windows/Mac/Linuxに対応しているので、環境に応じたインストーラーをダウンロードして実行します。
 - [appium/appium-desktop Releases](https://github.com/appium/appium-desktop/releases)

## テストコード
今回はFlutterのサンプルプロジェクトをテストしてみます。これは、右下の+ボタンをタップすると、中央の数字がカウントアップするという単純なアプリです。

今回はテストコードをJavaScriptで書き、Node.jsで実行することにします。
以下のコマンドで必要なモジュールをインストールします。
```bash
$ npm install webdriverio
```

テスト内容は、ボタンを2回タップしたときに「2」が表示されることを確認してみようと思います。
```JS
// index.js

/**
 * Flutterのサンプルアプリをテストするコード
 * 
 * Android StudioでProject nameを「remote_test_kit_sample」、
 * Organizationsを「com.example」（デフォルト）で作成した場合を想定
 */

const wdio = require("webdriverio");
const assert = require("assert");

const opts = {
  path: "/wd/hub",
  port: 4723,
  capabilities: {
    platformName: "Android",
    platformVersion: "12",  // 端末のOSバージョンに応じて指定
    deviceName: "Android Emulator",
    app: "/path/to/apk", // ローカルPC内のテスト対象apkファイルを指定する
    appPackage: "com.example.remote_test_kit_sample",
    appActivity: ".MainActivity",
    automationName: "UiAutomator2",
    // appium-uiautomator2-server-***.apkのインストールがタイムアウトする場合は以下のコメントを外す。デフォルトは20000ms。
    // uiautomator2ServerInstallTimeout: 40000,
  },
};

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

  const button = await client.$(
    '//android.widget.Button[@content-desc="Increment"]'
  );

  // 2回ボタンをタップ
  await button.click();
  await button.click();
  await client.pause(1000);

  const label = await client.$(
    '//android.view.View[starts-with(@content-desc, "You")]/following-sibling::android.view.View[1]'
  );
  const count = await label.getAttribute("content-desc");

  // 「2」が表示されること
  assert.strictEqual(count, "2");

  await client.deleteSession();
}

main();

```

## テスト実行
それでは実際にRemote TestKitで端末をレンタルし、実機上でテストを実行します。

まずは、Remote TestKitの画面上からテストしたい端末を選択し、「レンタル」ボタンをクリックします。今回はPixel 6(Android 12)を使用しましたが、端末のOSバージョンに応じて`capabilities`の`platformVersion`を書き換える必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20639/bec0fee2-f3ca-f69c-9219-2f5b24254be6.png)

端末の画面が表示されたら、AppiumがAndroid Debug Bridge（adb）を通して端末と通信できるよう、デバイス→仮想adb有効化をクリックします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20639/10c284aa-b02e-ead3-5054-27d8cceaf682.png" width="300" />


つぎに、Appiumサーバーを起動します。デフォルト設定でスタートさせておきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20639/7056b8fb-5c0d-535e-db96-3481520fa1f8.png" width="300" />　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20639/7368923c-2c34-6599-6766-99e9d5a1473f.png" width="300" />

それでは、アプリのテストを実行します。
端末へのapkインストール、テスト実行、結果の表示までが自動で行われます。
```bash
> node index.js
2022-07-10T09:04:14.593Z INFO webdriver: Initiate new session using the WebDriver protocol
2022-07-10T09:04:14.632Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session
2022-07-10T09:04:14.633Z INFO webdriver: DATA {
  capabilities: {
    alwaysMatch: {
      platformName: 'Android',
      platformVersion: '12',
      deviceName: 'Android Emulator',
      app: 'C:\\<省略>\\flutter-apk\\app-release.apk',
      appPackage: 'com.example.remote_test_kit_sample',
      appActivity: '.MainActivity',
      automationName: 'UiAutomator2',
      uiautomator2ServerInstallTimeout: 40000
    },
    firstMatch: [ {} ]
  },
  desiredCapabilities: {
    platformName: 'Android',
    platformVersion: '12',
    deviceName: 'Android Emulator',
    app: 'C:\\<省略>\\flutter-apk\\app-release.apk',
    appPackage: 'com.example.remote_test_kit_sample',
    appActivity: '.MainActivity',
    automationName: 'UiAutomator2',
    uiautomator2ServerInstallTimeout: 40000
  }
}
2022-07-10T09:04:24.021Z INFO webdriver: COMMAND findElement("xpath", "//android.widget.Button[@content-desc="Increment"]")
2022-07-10T09:04:24.022Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/c030b72e-9d2b-4674-ba3a-9d3e6af33517/element
2022-07-10T09:04:24.022Z INFO webdriver: DATA {
  using: 'xpath',
  value: '//android.widget.Button[@content-desc="Increment"]'
}
2022-07-10T09:04:24.676Z INFO webdriver: RESULT {
  'element-6066-11e4-a52e-4f735466cecf': '00000000-0000-0056-0000-000800000003',
  ELEMENT: '00000000-0000-0056-0000-000800000003'
}
2022-07-10T09:04:24.681Z INFO webdriver: COMMAND elementClick("00000000-0000-0056-0000-000800000003")
2022-07-10T09:04:24.681Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/c030b72e-9d2b-4674-ba3a-9d3e6af33517/element/00000000-0000-0056-0000-000800000003/click
2022-07-10T09:04:24.749Z INFO webdriver: COMMAND elementClick("00000000-0000-0056-0000-000800000003")
2022-07-10T09:04:24.749Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/c030b72e-9d2b-4674-ba3a-9d3e6af33517/element/00000000-0000-0056-0000-000800000003/click
2022-07-10T09:04:26.793Z INFO webdriver: COMMAND findElement("xpath", "//android.view.View[starts-with(@content-desc, "You")]/following-sibling::android.view.View[1]")
2022-07-10T09:04:26.793Z INFO webdriver: [POST] http://127.0.0.1:4723/wd/hub/session/c030b72e-9d2b-4674-ba3a-9d3e6af33517/element
2022-07-10T09:04:26.793Z INFO webdriver: DATA {
  using: 'xpath',
  value: '//android.view.View[starts-with(@content-desc, "You")]/following-sibling::android.view.View[1]'
}
2022-07-10T09:04:26.996Z INFO webdriver: RESULT {
  'element-6066-11e4-a52e-4f735466cecf': '00000000-0000-0056-0000-000500000003',
  ELEMENT: '00000000-0000-0056-0000-000500000003'
}
2022-07-10T09:04:27.000Z INFO webdriver: COMMAND getElementAttribute("00000000-0000-0056-0000-000500000003", "content-desc")
2022-07-10T09:04:27.000Z INFO webdriver: [GET] http://127.0.0.1:4723/wd/hub/session/c030b72e-9d2b-4674-ba3a-9d3e6af33517/element/00000000-0000-0056-0000-000500000003/attribute/content-desc
2022-07-10T09:04:27.093Z INFO webdriver: RESULT 2 # <================　テスト成功!
2022-07-10T09:04:27.094Z INFO webdriver: COMMAND deleteSession()
2022-07-10T09:04:27.094Z INFO webdriver: [DELETE] http://127.0.0.1:4723/wd/hub/session/c030b72e-9d2b-4674-ba3a-9d3e6af33517
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20639/53c20b70-6e3d-0aac-8e62-b0f32e9c555b.gif" width="600" />

:::note
エラーが発生する場合は端末を再起動してから実行してみてください。
:::


テストが失敗することも確認しておきます。2回ボタンをタップすると「3」が表示されることを期待したテストを実行してみます。
```JS
// index.js

// 「2」が表示されること
assert.strictEqual(count, "3"); // <=========== 3に変更
```
すると、期待値と結果が異なるためテストが失敗したことを表示してくれます。
```bash
$ node index.js
# 中略
2022-07-10T11:28:06.096Z INFO webdriver: RESULT 2
node:internal/process/promises:279
            triggerUncaughtException(err, true /* fromPromise */);
            ^

AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

'2' !== '3' # <============= テスト失敗!

    at main (D:\<省略>\index.js:42:10)
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  generatedMessage: true,
  code: 'ERR_ASSERTION',
  actual: '2',
  expected: '3',
  operator: 'strictEqual'
}
```

# まとめ
Remote TestKitを利用すると、クラウド上からスマホをレンタルし実機でアプリを検証できます。Appiumを使うと1つのテストコードで様々な機種についてテストを自動化できます。
今回はスマホのレンタル操作は手動で行いましたが、CI環境でレンタル操作も含めてテストを自動化できる「[Remote TestKit Appium Cloud](https://appkitbox.com/testkit/doc/appium_tutorial/)」というサービスもあるので、気になる方は試してみてください。