Appium初心者が、ReactNativeアプリを自動テストする試みの一部始終(随時更新)

# 本記事の目的

弊社	(リプレックス株式会社)　の関わっているプロダクトで、ReactNativeを使って作ったアプリがあるのですが、保守工数の削減のためにE2Eテストの自動化にチャレンジしてみることにしました。その際の技術選定の経緯なども含め、メモとして残すために書いています。

単なる殴り書きではなく、これからスマホアプリのE2Eテストの自動化を始めたい方が読む時に参考になる資料としても使えるように心がけて書いていこうとおもいます。

# E2Eテストとは
E2Eテストとは、　end to end test (エンドツーエンドテスト) の略です。end to endとは辞書を引くと「端から端まで」という意味です。UIテストはUI部分だけのテストですし、ロジックテストはロジック部分だけのテストですが、 **E2Eテストは、システムが完成した状態で、UI,DB,Networkなど、全体がくっついた状態のテストを行うこと** です。

このようなE2Eテストは、多くの場合、テスターさんに実際にアプリを使ってもらってテストすることが多いと思います。人間がやればいろいろ柔軟にやってくれるので、多くの現場では人員を投入してテストすることが多いと思います。

しかし、ずっとやっていくと以下のような問題も出てきます。

* リリースのたびに通しでテストを回すと数日〜数週間テストにかかって、コストもかかるじ、リリースに時間がかかる
* 多言語テストをしようと思うと、テスターさんが読めない言語で操作するのがつらい
* テスターさんによってみてくれるポイントが違ったりして、ばらつきが出る
   * 特に、「以前となんか表示が違う」みたいな細かいところに気づくかどうかは人による
* かと言ってテスト仕様書を細かく書き始めるとそれはそれで大変
* いくらテスターさんと言っても、同じような操作を何度もやるのは辛い

上記問題を解決するためには、テスターさんが手で操作する代わりに、プログラムでスマホを操作して、テストを自動化できると良いです。これを **E2Eテストの自動化** と呼びます。

# E2Eテストの自動化方法

E2Eテストを自動化する方法にはいろいろあるのですが、大きく分けて、無償のツールを手元のパソコン上で動かして実施する方法と、有償のオンラインサービスにアプリをアップロードしてテストする方法があります。

## 無償のツールを手元のパソコン上で動かして実施する方法

無性のE2Eテスト自動化ツールとしては Appium が有名です。本記事でもこのAppiumを使う方法を紹介していきます。 *後述する有償のオンラインサービスでも、裏側では Appiumが使われていることが多いと思いますので、Appiumの知識は持っていて損になることはないと思います。*

## 有償のオンラインサービスにアプリをアップロードしてテストする方法

あまり知見がないので、 CodeZineさんの [E2Eテスト自動化ツールの最新トレンド──Playwright？ ノーコード？ Seleniumから多極化の時代へ！](https://codezine.jp/article/detail/18289) という記事を拝見しました。

こちらの記事で2018年以降くらいに書かれている「多極化の時代」に出てきている以下のようなツールが有名なようです。さらにそれらを「コード型」と「ノーコード型」に分けて分類してみます。

* コード型
    * Cypress
    * Puppeteer
    * Playwright
* ノーコード型
    * [MagicPod](https://magicpod.com/)
    * mabl
    * Autify

通常、テストを自動化するためには、テスト自体をプログラムで書く必要があります。つまり「コード」が必要なので「コード型」といいます。以前は当たり前だったのでわざわざコード型なんて言っていなかったのですが、 **近年「ノーコード型」というプログラムを書かなくてもテストの自動化ができる方法が出てきた** ことで、区別するために「コード型」と呼ぶようになっています。

これらのサービスも別途検討はしているのですが、まずは、無償ツールの Appiumの検討を進めることにしました。

# Appiumの位置付けと特徴

公式サイトは https://appium.io/ です。こちらにドキュメントがあるので、 [Introductionのページ](https://appium.io/docs/en/2.1/intro/) などを一度読んでみることをお勧めします。

が、重要な部分をかいつまんでこちらに書いておきます。

## Appiumの前身である Selenium

先ほどの CodeZineさんの記事に **Seleniumの時代** ということが書かれていましたが、Seleniumというのは古くからあるUIテスト自動化のためのツールです。表では2012年頃から線が引かれていましたが、私がまだ20代だった2000年代初頭の頃からこの単語は聞いていた覚えがあるので初出はもっと古いと思います。 Wikipediaを見てみたところ、2004,5年ころから存在するようです。

こちらは主に Webサイトをテストするためにブラウザを自動操作してテストすることを主眼に置いたツールです。ブラウザを自動操作することで、WebアプリのE2Eテストの自動化をすることができます。


## スマホアプリのE2Eテストへの発展

2008年に iPhone 3Gが発売されて、アプリ開発が一般のデベロッパーに開放されて以来、多くのスマホアプリが作られるようになりました。当然スマホアプリのE2Eテストが必要になりますが、SeleniumはあくまでWebブラウザを操作することでテストを自動化するものだったため、アプリのテストができません。

そこで Seleniumの技術を応用して生まれたのが Appiumです。ドキュメントに書いてあるかわかりませんが、 App + Selenium で Appium という名前になったのだろうと想像できます。

こう書くと、

* Webアプリのテスト → Selenium
* スマホアプリのテスト → Appium

なのかと思ってしまいますが、 **Appiumは 「すべてのプラットフォームのテストを自動化するにはどうすればいいか？ (How do we enable automation for all the platforms?)」 という疑問に答えるために作っている**とあり、必ずしもアプリのためだけにあるものではないことが読み取れます。

Appiumがどうやってそれを実現しようとしているのかをしっかり理解した方が後の理解が深まると思いますので、可能な範囲で解説したいと思います。

## E2Eテストの自動化を行う上でのハードル

E2Eテストの自動化を実現するためには、さまざまなハードルを超える必要があります。例として、

* スマホのホーム画面でアプリAを探し、
* アプリAのアイコンをタップして起動し、
* 出てきた画面のスクリーンショットを残す

というテストを自動化する方法を考えます。

考えられる方法は、ロボットアームとカメラを使って、人間のように実際のスマホの画面を見てアイコンを探し、それをタッチして、アプリを起動し、さらにスマホのサイドボタンを同時に押してスクリーンショットを残すという方法です。

これは頑張ればできるでしょうが、簡単でないことは想像できると思います。

当然 Seleniumでもそんなことはしていません。画面上のボタンをカメラで見つける代わりに、Webブラウザのテスト用のAPIを使用します。押したいボタンが「閉じる」というボタンだとすると、ブラウザのAPIに「"閉じる"というボタンを見つけて」というリクエストを投げ、見つかったらそのボタンに「"クリック"という操作をして」というリクエストを投げます。最後に「スクリーンショットを残して」というリクエストを投げれば完成です。

JavaScriptやCSSを使ったことがある方であれば、HTMLの特定の要素をidやclassを使って特定したことがあると思います。Seleniumで要素を指定する方法もそれらと似たような方法で行います。

Seleniumでは、こうやってカメラやロボットアームを使わずにWebアプリのE2Eテストの自動化を実現しています。

この、

* "閉じる"ボタンを見つけて、
* それに "クリック"という操作をして、
* スクリーンショットを残して

という一連のコマンドを書いたプログラムが **テストプログラム** です。つまり「コード型」のE2Eテストですね。このテストコードはJavascript、Python、Ruby、Javaなど、さまざまな言語で書くことができます。

Appiumの場合も同じような方法でアプリのE2Eテストの自動化を行います。この場合、対象となるのはWebブラウザではなく、スマホOSということになります。iPhoneなら iOS、Androidなら Android OSに対して命令を送って、画面上のボタンなどを見つけ、それを押してもらうということをして自動化を実現します。

「え、Webじゃなくてスマホアプリにそんな仕組みあるの？」と思われる方もいるかと思います(私もそうでした)。実は、iOSやAndroidには以下のような自動化の仕組みをがあり、Appiumはそれらを利用しています。

* iOSの場合
    * XCUITest
        * https://developer.apple.com/documentation/xctest/user_interface_tests
* Androidの場合
    * UIAutomator
        * https://developer.android.com/training/testing/ui-automator?hl=ja
    * adb
        * https://developer.android.com/studio/command-line/adb?hl=ja


## Appiumのアーキテクチャ

Appiumのアーキテクチャの説明を読むと、 「Selenium WebDriverのAPIをベースに作られている」とあります。その理由も書かれていて、「すでに多数の E2Eテスト自動化の仕組みが Selenium向けに作られているため、なるべくそれらを再利用したいから」、とのことでした。

たとえば、先ほどの Webアプリのテストコード

* "閉じる"ボタンを見つけて、
* それに "クリック"という操作をして、
* スクリーンショットを残して

を Javascriptで書いたとしましょう。もしこのWebアプリと全く同じ画面構成のスマホアプリがあった場合、できればこのテストコードの部分はそのまま再利用したいと思いませんか？

その再利用ができるようにするために、Appiumは Selenium WebDriverとしてふるまうように作られています。Appiumはテストクライアントから受け取った命令を iOSの XCUITestや Androidの UIAutomatorのコマンドに変換することで、テストコードをそのまま使ってアプリのテストができるようにしています(あくまでイメージです。実際に何も弄らなくていいかどうかはケースバイケースです)

こうすることで、既存のSeleniumのテストコードが使っているライブラリ（=テストクライアントライブラリ)の資産をそのまま活用できるメリットがあります。

## Appiumをスマホアプリ以外にも拡張する

そう考えると、Appiumの頑張り次第で、iOS/Android以外のプラットフォームに対応できそうだということがわかりますでしょうか？ たとえば Kindleタブレットのテストに使いたいとなったら、Kindleタブレット側にUI操作のAPIの仕組みを用意し、Appiumがそれに変換するようにすればOKです。

もしあたなが独自のハードウェアをもっていてそのテストを自動化したいとなった場合は、そのためのUI操作のAPIを用意し、Appiumに接続できるようにすれば実現できます。

**この接続部分を「Appiumドライバー」と呼びます。** 

AppiumはAppiumドライバーさえ用意すれば、どんなプラットフォームのテストでも対応できるのだとういことがお分かりいただけたでしょうか。



# Appiumを使ってみる

ものは試しで、まずは Appiumを動かしてみることが重要です。動かしてみることでいろいろな気づきが得られるためです。そして、こういう時はまずは公式のチュートリアルを実施するのが鉄則ですので、 [Quick Start](https://appium.io/docs/en/2.1/quickstart/) を実施してみました。

Quick Startでは、Androidエミュレータをパソコン上で起動し、その上で動く Android OSの「設定」アプリを開いて「バッテリー」の項目を表示するという内容になっています。Appiumではこのように、アプリ単体のテストだけでなく、OSそのものの操作もできるようになっています。

## 実験環境

私は以下の環境で試しています。

* PC
    * Mac mini 2021 (M1 CPU)
* OS
    * macOS Sonoma 14.1

## Appiumのインストールと起動

チュートリアルでは

```
> npm i --location=global appium
```

で Appiumをインストールし、

```
> appium
```

で起動するとありましたが、私の場合は、

```
> npx appium
```

として、 npx環境で実行するようにしました。

Appiumが起動すると、いろいろなコマンドを受け付けられる待機状態になります。これを **Appiumサーバー** と呼びます。そして、これを利用するテストプログラムのことを **Appiumクライアント** と呼びます。

Appiumサーバーが起動しただけでは操作対象が何もないため、Appiumドライバーをインストールする必要があります。今回は Androidの操作を行いたいので、 以下のコマンドで、**Androidを操作するための UIAutomator2 Driveをインストールします。**

```
> appium driver install uiautomator2
```

npx環境の場合は、

```
> npx appium driver install uiautomator2
```

となります。

また、Androidエミュレータなども必要になるので、一連の Android開発環境もインストールします。アプリ開発エンジニアの方であればすでにインストールされているかと思いますが、そうでない場合はそれらのセットアップも必要になります。

さて、これでもう一度 Appiumサーバーを起動し直してみると、以下のようなメッセージがでて、UIAutomator2ドライバーが使えるようになっていることがわかります。

```
> npx appium           
[Appium] Welcome to Appium v2.1.3
[Appium] Attempting to load driver uiautomator2...
[debug] [Appium] Requiring driver at /Users/ohnaka/.appium/node_modules/appium-uiautomator2-driver
[Appium] Appium REST http interface listener started on http://0.0.0.0:4723
[Appium] You can provide the following URLs in your client code to connect to this server:
[Appium] 	http://127.0.0.1:4723/ (only accessible from the same host)
[Appium] 	http://192.168.11.30:4723/
[Appium] 	http://192.168.11.13:4723/
[Appium] 	http://10.90.3.2:4723/
[Appium] Available drivers:
[Appium]   - uiautomator2@2.31.3 (automationName 'UiAutomator2') 👈⭐️⭐️⭐️⭐️
[Appium] No plugins have been installed. Use the "appium plugin" command to install the one(s) you want to use.
```

## 初めてのテストコード

チュートリアルには、Javascript(JS)、Python、Java、Rubyの例がありますが、ひとまず Javascriptで試してみます。こちらの [Write a Test (JS)](https://appium.io/docs/en/2.1/quickstart/test-js/) のページを見てください。

適当なワークディレクトリを用意し、まずはそこで

```
> npm init
```

を実行します。そして、Appiumサーバーに接続するためのクライアントライブラリ、つまり WebDriveアクセス用のライブラリである `webdriverio` をこのテストプログラムに追加します。

```
> npm i --save-dev webdriverio
```

こうすると、 packages.jsonが以下のようになっているはずです。

``` JSON
{
  "devDependencies": {
    "webdriverio": "^8.11.2"
  }
}
````

そして、チュートリアル通り、テストコードを記述します。以下の全コードを引用します。

``` JavaScript
const {remote} = require('webdriverio');

const capabilities = {
  platformName: 'Android',
  'appium:automationName': 'UiAutomator2',
  'appium:deviceName': 'Android',
  'appium:appPackage': 'com.android.settings',
  'appium:appActivity': '.Settings',
};

const wdOpts = {
  hostname: process.env.APPIUM_HOST || 'localhost',
  port: parseInt(process.env.APPIUM_PORT, 10) || 4723,
  logLevel: 'info',
  capabilities,
};

async function runTest() {
  const driver = await remote(wdOpts);
  try {
    const batteryItem = await driver.$('//*[@text="Battery"]');
    await batteryItem.click();
  } finally {
    await driver.pause(1000);
    await driver.deleteSession();
  }
}

runTest().catch(console.error);
```

これを `test.js` として保存し、 

```
> node test.js
```

として実行すると、テストが始まります。その際、Androidの実機、もしくはエミュレータが接続されている(adbで見えている)状態にしておく必要があります。

なお、後述しますが、ホスト名が `localhost` だとなぜかうまく繋がらない現象が発生しまして、以下のように `127.0.0.1` を指定することでうまく接続できるようになりました（原因調査中)。

```
> env APPIUM_HOST=127.0.0.1 node test.js
```

環境変数を使わず、 `test.js` の `wdOpts` を直接書き換えてもOKです。

このテストコードを解説します。

### capabilities

このテストを実行する上での前提条件が書かれています。見るとなんとなくわかると思いますが、以下のような指定がされています。

* platformName: 'Android'
    * Android環境で実行したいという宣言
* 'appium:automationName': 'UiAutomator2'
    * UIAutomator2ドライバを使って操作するよという宣言 （これがAppiumサーバーになければエラーになる)
* 'appium:deviceName': 'Android'
    * 実行デバイスも Androidを指定
* 'appium:appPackage': 'com.android.settings'
    * テスト対象のアプリパッケージ名
    * 今回は標準設定アプリのパッケージ名を指定している
* 'appium:appActivity': '.Settings'
    * Androidのアクティビティ名

サンプルにはありませんが、言語や地域の設定（ロケールの指定)もここで行うことができます。

* 'appium:language': 'ja'
    * 言語を日本語で実行したいと言う宣言
* 'appium:locale': 'JP'
    * 地域を日本にして実行したいと言う宣言

この指定を変えることで、簡単に別言語でアプリをテストすることが可能です。当初、スマホの言語設定をどうやって変えたらいいのか……と悩んでいて、設定アプリを操作して変えるしかないのかと思ったのですが、GPTさんに聞いたら capabilitiesで変更できると言うことを教えてもらえました👏

なお、このようにテスト実行環境として要望する条件のことを **Desired Capabilities** と呼ぶようで、テスト起動時に以下のようなログが現れます。

```
2023-11-14T01:46:09.216Z INFO webdriver: Initiate new session using the WebDriver protocol
2023-11-14T01:46:09.218Z INFO @wdio/utils: Connecting to existing driver at http://localhost:4723/
2023-11-14T01:46:09.582Z INFO webdriver: [POST] http://localhost:4723/session
2023-11-14T01:46:09.582Z INFO webdriver: DATA {
  capabilities: {
    alwaysMatch: {
      platformName: 'Android',
      'appium:language': 'ja',
      'appium:locale': 'JP',
      'appium:automationName': 'UiAutomator2',
      'appium:deviceName': 'Android',
      'appium:appPackage': 'com.android.settings',
      'appium:appActivity': '.Settings'
    },
    firstMatch: [ {} ]
  },
  desiredCapabilities: {
    platformName: 'Android',
    'appium:language': 'ja',
    'appium:locale': 'JP',
    'appium:automationName': 'UiAutomator2',
    'appium:deviceName': 'Android',
    'appium:appPackage': 'com.android.settings',
    'appium:appActivity': '.Settings'
  }
}
```

ログには、 `capabilities`と `desiredCapabilities` の両方が出ていますが、おそらく以下のようなことだろうと思います。

* capabilities
    * 実際に決定された実行環境
* desiredCapabilities
    * Appiumクライアントが要求した実行環境

## wdOpts

`wd` は Web Driverの略ですかね。 なので、`wdOpts` は Web Driverのためのオプション（設定）値と考えて良いと思います。

* hostname
    * Appiumサーバーが動いているホスト名です
    * 環境変数の `APPIUM_HOST` が空の場合、`localhost`が使用されます
    * 理由はわからないのですが、なぜか `localhost` だと以下のようなエラーが出てうまく接続できないことがあり(macOSのセキュリティのせい？)、 `127.0.01` を指定したら接続できるようになりました。
        * `Unable to connect to "http://localhost:4723/", make sure browser driver is running on that address.`
* port
    * Appiumサーバーのポート番号です
    * 環境変数の `APPIUM_PORT` が空の場合、`4723`が使用されます
* logLevel
    * ログレベルの指定です
* capabilities
    * 上で定義した capabilities を渡します

## テストコード本体

テストコード本体は、まず、上記 `wdPots` を引数として `webdriverio` の `remote` を呼び出し、得られた `driver` に対して、命令を送ることでテストを行います。

この例では、 capabilitiesによって設定アプリ (`com.android.settings`) のアクティビティ (`.Settings`) が起動するところまでは前提条件として準備済みとなっていますので、次のステップとしては、画面内になる「バッテリー」というメニューを探してタップ（クリック)するようにしています。

```
const batteryItem = await driver.$('//*[@text="Battery"]');
await batteryItem.click();
```

この `$('//*[@text="Battery"]')` というのが、「テキストが "Battery" の要素を見つける」と言う意味になっています。

## 多言語への対応

さて、さきほどの Desired Capabilitiesを使って、日本語環境でテストしてみたいと思います。capabilitiesのところを以下のように書き換えます。

```
const capabilities = {
  platformName: 'Android',
  'appium:language': 'ja',
  'appium:locale': 'JP',
  'appium:automationName': 'UiAutomator2',
  'appium:deviceName': 'Android',
  'appium:appPackage': 'com.android.settings',
  'appium:appActivity': '.Settings',
};
```

これで実行すると、設定アプリが日本語で起動します……!が、バッテリー画面に入らずに終わってしまいます。なぜでしょうか？

ここまで注意深く読んできた方は気づいたかもしれませんが、原因は以下の部分です。

```
const batteryItem = await driver.$('//*[@text="Battery"]');
```

これは、「テキストが "Battery" の要素を見つける」というものでした。言語を日本語にしたことで、メニューの名称が「Battery」から「バッテリー」に変わってしまい、要素がマッチしなくなってしまったのです。

このようなことが起こるため、多言語評価を視野に入れる場合は、 **画面上の要素を選択する際に表示されている文字列は使わずに、オブジェクトのIDを使って選択するようにする必要があります**。

多言語評価をしない場合でも、ボタンの文言を変えるだけでテストが通らなくなってしまうため、なるべくIDを使って指定する方が望ましいです。

IDって何？と言う感じになると思いますが、こちらを解説する前に、 Appium Inspectorをご紹介したいと思います。

# Appium Inspector

Macや Windows上で動く GUIのツールです。以前は Appium Desktop などとも呼ばれていたようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41616/a1892e85-9fca-1b7b-c46e-e8ff81dfc689.png)

このあたり ( https://github.com/appium/appium-inspector/releases )からダウンロードしてインストールしてください。

これを使うとかなり作業が捗りますので、必ず入れた方がいいです。

## Appium Inspctor から Appium Serverに接続する

上記スクリーンショットのように、Desired Capabilitiiesのところに、以下の設定を入れてみましょう。

```
{
  "platformName": "Android",
  "appium:language": "ja",
  "appium:locale": "JP",
  "appium:automationName": "UiAutomator2"
}
```

左のリスト（表)で入れても良いですし、右のJSON Representationの鉛筆マーク(✏️)から直接 JSONを入れてもOKです。

入れたら「Start Session」を押すとAppiumサーバーと接続し、以下のように、繋がっている Android端末の画面が表示されるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41616/6f8e786a-fa89-6309-2a12-0e8ea462e6dc.png)

これまでのチュートリアルを行っていれば繋がるはずですので、繋がらない方は上の方を読み直してみてください。

うまくつながったら、実際のアプリを操作してみましょう。自分のアプリではなく、皆さんの端末にも入っていると思われる Google アプリを例にしてみます。Googleアプリを起動した後、 Appium Inspectorのリフレッシュボタン(🔄）を押すと、Inspector内の画面が更新されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41616/9ab470d8-f818-0062-6e5b-d2e96d5361f0.png)

この画面内で様々な要素をクリックすると、その詳細情報が3ペインの一番右に表示されますので、いろいろクリックしてみてください。

なお、端末側で操作しても、Appium Inspector側では変化は起こりませんので、リロードしたい場合はリフレッシュボタン(🔄）を都度押す必要があります。

## 画面内から要素を見つけ出す方法

少し話を戻します。アプリのE2E自動テストを行う際に、どうやって画面上の要素を特定するのかが重要でした。そのためには各要素を表す特徴を実際に見てみるのが早いのですが、この Appium Inspectorを使えば、画面をクリックするだけで簡単に調べることができてとても便利です。

ためしに、画面左下の「発見」ボタンを選択する方法を調べてみましょう。上記スクリーンショットのように「発見」をInspector上で選択すると、画面右にプロパティが表示されます。この中で、要素の特定に使えそうないくつかの値を列挙してみました。

| キー | 値 |
|:-:|:-:|
| accessibility id  | 発見  |
| id | com.google.android.googlequicksearchbox:id/googleapp_navigation_bar_discover  |
| xpath  | //android.widget.FrameLayout[@content-desc="発見"]  |
|  elementId | 00000000-0000-0136-ffff-ffff0000002f  |
| content-desc  | 発見  |
| resource-id  | com.google.android.googlequicksearchbox:id/googleapp_navigation_bar_discover  |

いろいろあって悩んでしまいますが、以下にそれぞれの特徴を示します（間違っている部分があるかもしれませんが、発見次第随時修正します）。

### accessiblity id

アクセシビリティというのは、スマホ操作のユーザー補助機能のことで、指先に障害のある方がキーボードを使わずにスマホを操作したり、視覚に障害のある方が画面の内容を音声で読み上げたりするような、そういった機能のことを指します。

ここに出てくる accessiblity idはUIの各要素につけることのできるIDで、アプリ設計者（プログラマー)がつけたIDです。この例では「発見」という日本語にローカライズされた値が入っているので、音声読み上げソフトなどを使用すると、カーソルをこの上に合わせた時にこの文字列が読み上げられるようになっているようです（試してはいません）。

一方、Appium関連のブログ記事などを読むと、要素を特定するためにこの accessibility idを使うことを推奨した記事なども見かけます。たとえばこちらの　「[Appium Accessibility IDs: Why and How to Set Them](https://www.waldo.com/blog/appium-accessibility-id)」 という記事にも書かれています。2022年の記事なので割と新しいのですが、私は **これは「間違った使い方」だと思っています。**

こちらのappiumのディスカッションサイトのスレッド「[React Native UI element access via testID](https://discuss.appium.io/t/react-native-ui-element-access-via-testid/7845/2)」は2016年と古いやり取りですが、その中でも **「accessibility idはTalkBack(視覚障害者むけの画面読み上げ機能のこと)での読み上げに使われたりするから、この値にはテスト用のIDはセットしたくない」** という意見が書かれています。

なぜ accessibility id をテストで使うようなことが広まってしまったのでしょうか？

Androidのこのアクセシビリティ関連の変遷についてまとめられたブログ 「[2021年 React Native のアクセシビリティを勝手に振り返る](https://zenn.dev/grgr_dkrk/articles/react-native-accessibility-2021)」 に、以下のような情報が書かれておりました。

> Android の testID が正しく認識されるようになった
> 0.64 では、Android における testID の扱いが改善されました。
> これのどこがアクセシビリティ、という話なのですが、大事な変更です。
>
> testID は Web と同じように E2E で使用されるものですが、Android では、testId が View.tag にセットされていました。それが、Appium[6] のドライバーである UI Automator で読み取れない問題を引き起こしました。
> そこで、Android で E2E テストを回す時の代替手段として accessibilityLabel をテスト ID 代わりに使用するという、誤ったプラクティスが横行しました。
> このプラクティスはポピュラーでして、個人ブログや React Native の Issue でも度々取り上げられ、見たことがあるかもしれません[7]。

どうもこういう経緯があったようです。

> この問題に関する PR は数年前から色々と出ていましたが、最終的には 2020 年に出た testID を resource-id と結びつける PR が採用されました。

とのことで、最新のReactNativeアプリでは修正が入っており、ReactNative上で `testID` としてセットしたものが、Android上では `resource-id` としてセットされ、 UIAutomatorで認識できるようになったようです。

### id
どういうプロパティなのかよくわかっていません。resource-idと同じ値が入っているようにも見えます。
全ての要素に必ず存在するわけでもないので、どうやらプログラム側からセットした場合だけ見える値のようです。

ある情報によると、

* Android
    * resource-idにセットしたものと同じものがAppium上で idとして見える
* iOS
    * nameにセットしたものがAppium上で idとして見える

ということのようです（要確認）。

### xpath
この要素に到達するための xpathです。これがいいんじゃないかと思ってしまいますが、どうも調べてみると xpathは壊れやすいので使いづらいとか、画面にまだ表示されていない場合は使えないとか、動作が遅いとか、いろいろ問題もあるようです。（要調査)

### elementId
なんかのUUIDが入っているように見えるのですが、いつ、誰がセットしたものなのかよく分かりません。要調査。

### content-desc
どうも accessibility id　と同じ値が見えているようです。

### resource-id
上記 accessibility id の説明で引用したブログによると、 ReactNativeアプリ上で testId としてセットしたものが、Android上では resource-idとして見えるようになったとあります。

ですので、 **この resource-id（もしくはそれが透過的に見える`id`)を使って要素を指定するのが本命と思われます**。

# Googleアプリをターゲットにしていろいろやってみる

だいぶ仕組みが見えてきたところで、Googleアプリをターゲットに、多言語対応したテストコードを実際に書いてみることにします。

## 「発見」タブを `recource-id` で指定して押してみる

まず最初に、先ほどの `resource-id`を使って実際に Googleアプリのタブを押してみたいとおもいます。それをするためには、[WebdriverIOのセレクタの説明](https://webdriver.io/docs/selectors/)を読んでみます。

色々な選択方法がありますが、おそらく `ID Attribute` を使う方法だと思いますので、先ほどのテストコードを以下のように書き換えてみます。

### Capabilities

``` JS
const capabilities = {
  platformName: 'Android',
  'appium:language': 'ja',
  'appium:locale': 'JP',
  'appium:automationName': 'UiAutomator2',
  'appium:deviceName': 'Android',
  'appium:appPackage': 'com.google.android.googlequicksearchbox',
  'appium:appActivity': '.SearchActivity',
};
```

Capabilitiesをこのように書き換えて、Googleアプリが起動するようにします。Googleアプリのパッケージ名とアクティビティ名は、アプリがフォアグラウンドにいる状態で、

```
> adb shell dumpsys activity activities
```

を実行して出てきた以下の出力から類推しました。

```
ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
  Stack #421: type=standard mode=fullscreen
  isSleeping=false
  mBounds=Rect(0, 0 - 0, 0)
    mResumedActivity: ActivityRecord{937e981 u0 com.google.android.googlequicksearchbox/.SearchActivity t421}
```

### テストコード

``` JS
async function runTest() {
  const driver = await remote(wdOpts);
  try {
    const collectionsItem = await driver.$('id=com.google.android.googlequicksearchbox:id/googleapp_navigation_bar_collections');
    await collectionsItem.click();
    await driver.pause(3000);
    const discoverItem = await driver.$('id=com.google.android.googlequicksearchbox:id/googleapp_navigation_bar_discover');
    await discoverItem.click();
    await driver.pause(3000);
  } finally {
    await driver.pause(1000);
    await driver.deleteSession();
  }
}
```

押したことがわかりやすいように、最初に「保存済み」タブを押して3秒待ち、その後「発見」タブを押すようにしています。各タブの idは Appium Inspectorを使って調べました。

書き換えたテストコードを実行すると、

```
> env APPIUM_HOST=127.0.0.1 node test.js
```

実際の端末でGoogleアプリが起動し、2つのタブを自動で行き来するのがわかると思います!

## `resouce-id`? それとも `id`?

WebdriverIOでは `id=` という指定をしていますが、`resource-id`ではないのでしょうか？私は今のところ、これを以下のように理解しています。

* resource-id
    * Androidという環境において、要素が持つプロパティの一つ
    * Googleアプリはここに要素を特定するためのIDをセットしている
    * ReactNativeアプリで `testId` をセットした場合も、この`resource-id`に値がセットされる（はず)
* id
    * Appiumのドライバのレイヤで見えるプロパティ
    * なんの値を idとして使うかはドライバ次第だが、おそらく以下のようになっている
        * Androidの UIAutomator2の場合、resource-idの値が idになる
        * iOSの XCUITestの場合 nameの値が idになる(と思われる)

ということで、Appiumのテストコードから見えるのはあくまで抽象化された `id` の方になるというわけです。以後、この資料でも `id` と呼ぶことにします。

## 英語で実行してみる

多言語対応をするためにだいぶ回り道をしてしまいました。今回、要素の指定を `id`を使うようにしたので、英語環境でも問題なく要素選択ができるはずです。

capabilitiesを以下のように修正して、英語モードで Googleアプリをテストしてみましょう。

``` JS
const capabilities = {
  platformName: 'Android',
  'appium:language': 'en',
  'appium:locale': 'US',
  'appium:automationName': 'UiAutomator2',
  'appium:deviceName': 'Android',
  'appium:appPackage': 'com.google.android.googlequicksearchbox',
  'appium:appActivity': '.SearchActivity',
};
```

このように書き換えて実行したら、英語モードでGoogleアプリが起動し、 **日本語の時と同じように「保存済み(Saved)」タブが選択された後、「発見(Discover)」タブが選択されました!**

これでようやく、アクセシビリティに影響を与えず、かつアプリの動作言語が変わっても影響を受けずにE2E自動テストを実施する目処が立ちました。

# テストフレームワークとして Jestを使ってみる

さて、アプリが当初の目論見通り自動で動かせるようになってきてひと安心……なのですが、 **これではまだテストとしては使いづらいです。**

一般的に、JUnitなどのテストフレームワークを使うと、IDE上でプログレスが出て **「100個中99個のテストに成功」** みたいな結果が青と赤のバーで表示されたりします。これをするためには、単に自動で動かすだけではなく **どうなったら成功で、どうなったら失敗なのか**を定義し、検証するコードも必要です。

また、多くのテストフレームワークでは、複数のテストケースに対して共通の事前コードや事後コードなどを書くこともできるので、大量のテストコードを書く際に冗長なコードを書かなくて済むようになります。

JUnitは Java用のテストフレームワークです。テストコードをJavaで書いて、JUnitを使ってもいいのですが、ここまで JavaScriptを使って書いてきましたし、ReactNativeもJavaScriptベースなので、JavaScriptで使えるテストフレームワークを使ってみることにします。

調べると Jestというのがよく使われるようなので、今回はこれを導入してみることにします。

# 以下執筆中

### 執筆予定

* Jest
    * セットアップ方法
    * 実行結果の検証をする方法
    * 同じテストケースを複数のデータセットで実行する方法
    * 異なる言語で実行し、画面上の要素が正しく翻訳されているかチェックする方法
    * テストごとにスクリーンショットを残し、エラーになった時の画面を素早く確認できるようにする
* 高度なUI操作
    * ジェスチャー、スクロールなどの高度な操作を行う方法
* Android/iOSのクロスプラットフォームアプリのテストのノウハウ
    * ReactNative
    * Flutter
* CI
    * 実機を接続せずに、CIサーバー上のシミュレータでE2Eテストを実施する方法
* MagicPodのような商用のE2Eサービスの検証