DetoxとAppiumでReact Nativeアプリのe2eテストを書いてみた

# はじめに
React Native製アプリのe2eテストを作成するにあたって[公式](https://reactnative.dev/docs/testing-overview#end-to-end-tests)で紹介されている2つのe2eテスト用ツールを使って比較してみました。

![detox.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/dcf79983-f996-0738-dcca-681ea03793c1.gif)


# テスト内容
テキストとボタンを1つずつ持つスクリーンがあり、ボタンをタップするとテキストの内容が変わることをテストします。

# Detoxについて
DetoxはReact Native用に作られたe2e用テストツールです。

# Appiumについて
React Nativeアプリに限らずネイティブアプリケーションのためのe2e用テストツールです。テストコードも好きな言語で書くことができ、テストのためにプロジェクトに手を入れる必要がありません。
Appiumはテストコードやツールを制限しないという理念があるため、Appiumサーバーを介してe2eテストを実行します。
シミュレーターでどのような操作をするかは、HTTPリクエストをAppiumサーバーに送信することで実現します。


# 実装
## Detox
基本的に[公式ドキュメント](https://wix.github.io/Detox/docs/introduction/getting-started)のとおりに進めればテストの追加をすることができました。
WebStormを使っていましたが、以下の設定がないとエディタ上でlintエラーになってしまいました。
```js:.eslintrc.js
module.exports = {
  root: true,
  extends: '@react-native-community',
  plugins: ['jest', 'detox'], // 追加
};
```

### 実際のテストコード
```js
/* eslint-env detox/detox, jest */
describe('Example', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should show "Ready?"', async () => {
    await expect(element(by.id('textId'))).toBeVisible();
    await expect(element(by.id('textId'))).toHaveText('Ready?');
  });

  it('should show "Go" after tap', async () => {
    await element(by.id('buttonId')).tap();
    await expect(element(by.id('textId'))).toHaveText('Go');
  });
});

```

プロジェクトにDetoxを追加したときのプルリクエストは[こちら](https://github.com/sekitaka/react_native_e2e_test/pull/7/files)



## Appium
基本的に[公式ドキュメント](https://appium.io/docs/en/about-appium/getting-started/index.html)どおりですが、React Native以外にも対応しているので、React Nativeをモバイルアプリして認識して、モバイルアプリのテストをどう作るかという部分だけ理解していくのに少し苦労しました。
iOS/Android以外のプラットフォームにも対応したテストツールなので、Appiumからそれぞれのプラットフォームを操作するためにプラットフォームごとのドライバの準備が必要です。

### ハマった点1
Appiumサーバーに対してテストコードを記述する側をAppiumクライアントと呼びます。
Javascriptにクライアントは`wd`という今は非推奨になったものと、`webdriverio`という今使われているものがあり、ぐぐるとドキュメントが混在しておりサンプルコードと同じ用に書いても動かないという経験をしました。

### ハマった点2
今回はReact nativeのステートを変更させることでUIを変更しています。
しかしボタンタップ直後はその変更が伝搬しておらず、意図したようにテストをパスしませんでした。(コードの`It needs to wait for reflecting state to UI.`の部分)
ボタンタップから少し待つことで問題を回避しましたが、すべてのテストでこのコードが必要だとちょっと面倒です。(テストコードが埋もれてしまう)

### ハマった点3
UI要素を選択するためのセレクタがWebのものがサンプルに出てくることが多いですが、今回はモバイルアプリだったのでそのまま使えないものが多かったです。公式ドキュメントがあるので[モバイルアプリ用のセレクタ](https://webdriver.io/docs/selectors/#mobile-selectors)を見ておけばよかったです。

### 実際のテストコード
```
/* eslint-env jest */
const wdio = require('webdriverio');
import {opts} from './config';

describe('Appium test', () => {
  let driver;
  beforeAll(async () => { });

  beforeEach(async () => {
    driver = await wdio.remote(opts);
  });
  afterEach(async () => {
    await driver.deleteSession();
  });

  afterAll(async () => {
    // await driver.quit();
  });

  const sleep = () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, 1000);
    });
  };

  it('should show "Ready?"', async () => {
    const button = await driver.$('~buttonId');
    const text = await driver.$('~textId');
    const beforeText = await text.getValue();
    expect(beforeText).toEqual('Ready?');
  });

  it('should show "Go" after tap', async () => {
    const button = await driver.$('~buttonId');
    const text = await driver.$('~textId');
    const beforeText = await text.getValue();
    await button.click();
    await sleep(); // It needs to wait for reflecting state to UI.
    const afterText = await text.getValue();
    expect(beforeText).toEqual('Ready?');
    expect(afterText).toEqual('Go');
  });
});

```


プロジェクトにAppiumを追加したときのプルリクエストは[こちら](https://github.com/sekitaka/react_native_e2e_test/pull/9/files)

# まとめ
React Nativeのe2eテストという意味ではDetoxを使うのが簡単でわかりやすいe2eテストがかけると思いました。
しかし1つのReact Nativeアプリだけでなく、Web含めてe2eテストのスキル自体を上げていきたいような場合にはAppiumを使うのもいいと思いました。Appiumの目指すところが新しいプラットフォームや言語が出てきても同じe2eテスト体験ができることだと思うので。

今回検証に使用したプロジェクトは以下で公開しています。
https://github.com/sekitaka/react_native_e2e_test