production.log

株式会社リブセンスでエンジニアをやっている星直史のブログです。

Managed workflowのExpoアプリに導入するE2Eテストフレームワークの比較と導入

概要

運用しているアプリの規模が大きくなってくると、ちょっとした修正が思わぬところに影響してしまったり、ツールのバージョンアップをする際に組織全体を巻き込んだ大規模な手動テストを行わなければならないことがしばしばあります。

自分が運用しているSnapmartアプリも例外ではありません。 SnapmartアプリはManaged workflowのExpoアプリであり、ejectする予定はありません。 今回はManaged workflowのExpoアプリに導入するE2Eテストフレームワークの比較と導入についての記事を書きます。

比較

React NativeにおけるE2Eテストフレームワークは以下の3つです。

  1. Appium
  2. Detox
  3. Cavy

SnapmartアプリをManaged workflowのExpoアプリとして運用していくことにこだわる理由は大きく2つです。

  1. iOSやAndroidに知見があるエンジニアが社内に少ない
  2. そもそもエンジニアが少ない

面倒な処理は極力Expoに押しつけ、iOS/Androidからは距離を置くことで、主戦場であるWebで相撲を取るという選択をしています。

そのためツール選ぶ際の制約として「expo ejectコマンドを避ける」が出てきます。
それを踏まえて考えてみます。

Appium

github.com

AppiumはSeleniumを使用して、クロスプラットフォームテストフレームワークです。 Seleniumを使ったことがあるエンジニアの方は多いのではないかと思います。

しかし、iOS/Android共にビルドしなければならず、Expo Clientで動かしながら開発が進められなさそうなので、断念しました。 これについて、ハックもできるようですが情報が少なすぎるので避けました。

Testing Your React Native App With Expo & Appium | by Colin Wren | The Startup | Aug, 2020 | Medium

Detox

github.com DetoxはWiが開発しているツールです。 Detoxの特徴の一つとしてFlakyなテストに対しての対策をツールが用意していることです。 Flakyなテストとは、例えば非同期処理や処理速度の問題で、テスト実行のたびに成功/失敗が分かれてしまうテストです。 Detoxはこれにたいして、内部で非同期処理を検知し、終了まで待機することで、回避しているようです。

当初Expoもhelperを用意していたようですが、2018年から開発が止まっています。 また、DetoxもExpoアプリのサポートをしていないと明言しています。

試しにdetox-expo-helpersを使って動かしてみましたが、そもそも64bitのSimulatorで動作しなかったので、断念しました。

Cavy

github.com CavyはReactのRefsを使用してテストを行うフレームワークです。 *1 テストランナーはjestに限定されます。

純粋なJavaScriptで書かれているので、yarn addをすればManaged workflowのExpoアプリでも導入することができます。 消去法になりましたが、Cavyに決定です。

Cavyの導入

基本的にはGettingStartedに書かれている内容と同じです。 ダイジェストでどうぞ!

cavy.app

前提

$ node -v 
v12.18.1

$ expo -V
3.27.4

$ cavy -V
1.1.0

インストール

$ yarn global add cavy-cli
$ yarn add cavy --dev #TypeScriptを使用している場合は、@types/cavyも追加
$ cavy init

設定

Cavyの登場人物は、大きく3つです。

  1. RootComponentをラップするTesterコンポーネント
  2. テスト対象(コンポーネント)
  3. テストコード

Cavyの動作

Cavyはcavy-cliを使用してテストを実行すると、以下のスクリプトが走ります。

$ cp App.js App.notest.js
$ mv App.test.js App.js

そして、テストが実行された後に、元に戻す処理が走ります。

$ cp App.js App.test.js
$ cp App.notest.js App.js
$ rm App.notest.js

1. RootComponentをラップするTesterコンポーネント

CavyはApp.jsをApp.test.jsで置き換えた後にテストを実行するため、まずはテストを動かすApp.test.jsを作ります。

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View, TextInput } from 'react-native';
+++ import TestableScene from './src/TestableScene';
+++ import { AppRegistry } from 'react-native';
+++ import { Tester, TestHookStore } from 'cavy';
+++ import ExampleSpec from './specs/exampleSpec';

+++ const testHookStore = new TestHookStore();

export default () => {
  return (
+++     <Tester specs={[ExampleSpec]} store={testHookStore}>
      <View style={styles.container}>
+++         <TestableScene />
        <Text>Open up App.js to start working on your app!</Text>
        <StatusBar style="auto" />
      </View>
+++     </Tester>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
+++ AppRegistry.registerComponent('cavyTest', () => AppWrapper);

2. テスト対象(コンポーネント)

次に、テスト対象のコンポーネント(例としてTestableSceneを用意しました)を修正します。

import React from 'react';
import { View, TextInput } from 'react-native';
import { hook } from 'cavy';

class Scene extends React.Component {
  render() {
    return (
      <View>
        <TextInput
          ref={this.props.generateTestHook('Scene.TextInput')}
          onChangeText={console.log("")}
        />
      </View>      
    );
  }
}

const TestableScene = hook(Scene);
export default TestableScene;

Cavyの特徴は、ReactのRefsを使用すると先に述べましたが、ここで登場です。
CavyはgenerateTestHookを使用して、TestHookStoreにRefsを渡すことで、テスト時にTextInputコンポーネントに対してテストを行える様にしています。

また、generateTestHookの引数として渡した文字列が、テスト実行時の対象コンポーネントの識別子となります。

上記のコードでは、hook()を使用しましたが、この他にも、React Hookを使用する方法や、refを割り当てることができない関数(Textコンポーネントとか)をwrap()でアプローチすることもできます。

cavy.app

3. テストコード

最後にテストコードです。

export default function(spec) {

  spec.describe('Logging in', function() {

    spec.it('works', async function() {
      await spec.exists("Scene.TextInput");
    });
  });
}

これはシンプルですね。 CavyがTest Helpersを用意しているので、わかりやすくなっています。

テスト実行

テスト実行はExpo Clientで行えます。

expo start --clearでSimulatorを立ち上げます。

その後、cavy run-ios --skipbuild --file App.jsを実行します。 ポイントとしては、--skipbuildオプションと--file App.jsでエントリーポイントを指定することです。 --skipbuildがなければ、ReactNativeのbuild用の処理が走ってしまうため、スキップをします。 --fileオプションは、Expoを使用している場合は、エントリーポイントがApp.jsとなっているため、Cavyのデフォルトであるindex.jsから変更するために指定します。

テスト結果はこの様に出力されます。 f:id:watasihasitujidesu:20200914211933p:plain

まとめ

AppiumとDetoxはReactNativeアプリのE2Eテストフレームワークの候補として挙がりましたが、Managed workflowのExpoアプリに導入するE2EテストフレームワークはCavy一択となりました。 Cavyはテスト実行がやや恐ろしいですが、簡単導入することができました。 ただ、用意されているTest Helpersがやや貧弱な印象*2を受けたので、少しお試しで使ってみようと思います。

*1:あと可愛いです

*2:例えば、スクリーンショットが使えないとか