Expo アプリでの Detox を使った End-to-End テストの実装2021/08/30

Expo アプリでの Detox を使った End-to-End テストの実装

タイトル通り「Detox を使った End-to-End テストの実装」 についての紹介です。

基本的にはドキュメント通りに進めれば実行可能です。しかし Expo を使ったアプリの場合、うまく動かない現象に遭遇しました。 その解決方法も紹介します。

概要

  • Detox とは
  • インストール
  • Expo アプリの場合に必要なもの
  • テスト実行
  • エラーがでてテストが正常に動作しない
  • 解決方法
  • Detox の簡単な使い方

Detox とは

https://github.com/wix/Detox

End-to-End テストを実装するためのパッケージです。

End-to-End テストとは、ユーザーインターフェーステストとも呼ばれる物です。システム全体に対してユーザーが実際に動かす操作を行い、期待した動作になっていることを確認します。 スナップショットテストやユニットテストとは異なり、実際にアプリをシミュレータ上で動かして操作を行い期待した動作になることをテストします。

より本番環境に近い形でのテストですね。シミュレータ上で行うので CI でのテスト実行も可能です。

インストール

まずは必要なパッケージをインストールします。 いつも通り npm・yarn で、テストでのみ使うので dev にインストールします。

npm install --save-dev detox detox-expo-helpers expo-detox-hook

Detox ではシミュレータを操作するので、必要なツールを brew でインストールします。

brew install --HEAD applesimutils

あとはコマンドラインで操作できるようにcliツールをグローバルにインストールしておきます。

npm install -g detox-cli

インストールが完了した後は以下のコマンドで Detox の初期設定を行うことができます。

detox init -r jest

e2e/ 以下に設定ファイルやテストケースが作成されます。

Expo アプリの場合に必要なもの

上記の init した時点で必要な設定やファイルが一式生成されます。 Expo でテストを行う場合、以下の対応も必要になります。

  • reloadApp に書き換え
    初期の状態では device.reloadReactNative() を各テストの前に呼んでいます。 これを detox-expo-helpersreloadApp に変更する必要があります。
const { reloadApp } = require('detox-expo-helpers');
~~~

describe('Example', () => {
  beforeEach(async () => {
    await reloadApp();
  });
~~~
  • Expo go App を用意する
    さらに、シミュレータで動かすにあたり Expo go アプリの app ソースも必要になります。
    Expo go のタウンロードページにて、IPAファイルを直接ダウンロードして解凍を行います。そのディレクトリを Expo.app という名前にリネームしておきます。
    プロジェクトのディレクトリに bin/ などのディレクトリを用意して上記 Expo.app を設置しておきます。

https://expo.dev/tools#client

その後 Detox の設定ファイル detoxrc.json に以下を追加します。configurations にて使用できるシミュレータを追加しています。name などは自分の使っているシミュレータに合わせて設定しましょう。

  "configurations": {
    "ios.sim": {
      "binaryPath": "bin/Expo.app",
      "type": "ios.simulator",
      "name": "iPhone 11"
    },
  }

テスト実行

テストを実行する際には Expo でエミュレータ上にアプリを起動しておき、以下のコマンドを実行します。 オプションで先程設定したシミュレータを使用するように設定しています。

detox test --configuration ios.sim

すると Detox でのE2Eテストが動き始めます。

エラーがでてテストが正常に動作しない

しかし、私の場合は最初のアプリのリロードは行われるのですが、以降の操作が実行されずテストの項目もチェックされない状態になっていました。 ただタイムアウトしてテストが失敗する状態です。

テスト結果には以下の出力がありました。

App has not responded to the network requests below:
  (id = -1000) isReady: {}

どうやら、detox-expo-helpersreloadApp を実行した際に、アプリの再起動が完了したことをリッスンしているのですが、その信号をキャッチできていないとのこと。 なので、アプリが起動しているにもかかわらずテストのチェックや操作が動き出さずそのままタイムアウトしています。

解決方法

解決方法を探したところ以下のissue にてやりとりされていました。

https://github.com/expo/detox-tools/issues/1

ざっくりの解決方法の方針としては、上記の内容を参考に以下のように行いました。

  • reloadApp にてアプリの起動完了チェックを行わない
  • アプリの起動完了チェックを自前で行いテストを始める

それぞれの詳細な対応について私の対応方法を以下に記載します。

reloadApp にてアプリの起動完了チェックを行わない

detox-expo-helpers に対して patch-package を使って、起動完了チェックを無効化するパッチを作成。

以下パッチの中身です。detox-expo-helpers+0.6.0.patch

diff --git a/node_modules/detox-expo-helpers/index.js b/node_modules/detox-expo-helpers/index.js
index 864493b..5de0839 100644
--- a/node_modules/detox-expo-helpers/index.js
+++ b/node_modules/detox-expo-helpers/index.js
@@ -70,7 +70,7 @@ const reloadApp = async (params) => {
     newInstance: true,
     url,
     sourceApp: 'host.exp.exponent',
-    launchArgs: { EXKernelDisableNuxDefaultsKey: true, detoxURLBlacklistRegex: formattedBlacklistArg },
+    launchArgs: { EXKernelDisableNuxDefaultsKey: true, detoxURLBlacklistRegex: formattedBlacklistArg, detoxEnableSynchronization: 0 },
   });

アプリの起動完了チェックを自前で行いテストを始める

テストケースのreloadAppが起動完了のチェックを行わなくなったので、直後に起動を確認する処理を追加します。 アプリの起動後に開く画面、ホーム画面などにtestID={"container"} のコンポーネントを用意しておき、それが表示されたことをチェックしています。

beforeAll(async () => {
    await reloadApp();
    await waitFor(element(by.id("container")))
      .toBeVisible()
      .withTimeout(4000);
  });

~~~

上記対応で、アプリの再読み込みと起動完了のチェックが毎テストの前に正常に行われるようになりました。

テストも正常にシミュレータが動き、テストケースが順番通り実行されました。

Detox の簡単な使い方

さまざまなAPIがありますが、簡単なものだけ紹介します。

  it("should have FAB", async () => {
    await expect(element(by.id("container"))).toBeVisible();
    await expect(element(by.id("FAB"))).toBeVisible();
  });

testID で要素を検出して、toBeVisible で画面に描写されていることをテストします。

  it("should show new memo screen after tap", async () => {
    await expect(element(by.id("FAB"))).toBeVisible();
    await element(by.id("FAB")).tap();
    await expect(element(by.text("create new memo"))).toBeVisible();
  });

testID で要素を検出して、tap で要素のタップイベントを実行できます。 シミュレータ上でも動作が行われ、その後の画面の要素の描写を確認することで、画面遷移などが行われたことをテストできます。

おわりに

Expo の場合はいくつか追加の設定が必要になりましたが、無事に Detox を利用できました。

エラーも発生したので Detox 使えないのかもと思いましたが、なんとか動かせました。 detox-expo-helpers の問題の様子?なので、いつか修正されるのを待ちましょう。修正されるまではとりあえずパッチを当てて確認するのがよいです。

E2Eテストが実行できるとインターフェース面でのテストが豊富に実施できるので、とても便利です。 ローカル環境だけでなくCI環境でもテストを動かせるようにして、変更のチェックを行えるとさらに便利になります。

  • このエントリーをはてなブックマークに追加