wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

react-nativeでe2eを実装してみる

シェアフル Advent Calendar 2018 17日目の記事です。

ある程度アプリの機能が揃ってきたので、そろそろテスト追加フェーズかなという感じでe2eの実装してみた

react-nativeでe2eの実装

react-nativeではDetoxを使ってe2eを実装していきます

github.com

また↓を使えばexpoでもe2eが可能です

GitHub - expo/detox-expo-helpers

detox-tools/packages/expo-detox-hook at master · expo/detox-tools · GitHub

Detox実装

事前準備

ここにある通りでOK

Detox/Introduction.GettingStarted.md at master · wix/Detox · GitHub

DEMOを動かしてみよう

こちらが、動かせるサンプルです

GitHub - expo/with-detox-tests: This template is currently out of date! If you'd like to maintain it, reach out to brent@expo.io

READMEの通りにセットアップすれば、こんな感じに動きます

https://raw.githubusercontent.com/expo/with-detox-tests/master/example.gif

早速アプリ組み込んでみる

インストール & 設定

mochaとjestどちらでも使えますが、ここではDEMOと同じでmochaを選択

npm i -D detox detox-expo-helpers expo-detox-hook mocha

初期テストコードをcliから追加

npm install -g detox-cli
detox init -r mocha

package.jsonにdetoxの設定を追加

"detox": {
  "configurations": {
    "ios.sim": {
      "binaryPath": "bin/Exponent.app",
      "type": "ios.simulator",
      "name": "iPhone 7"
    }
  }
}

package.jsonにe2eのコマンドを追加

  "scripts": {
    "e2e": "detox test --configuration ios.sim"

これで初期設定はOK

テストコード

テストの書き方

アプリ側に 「testID」を埋め込んでテストコード側で、そこを対象に操作していきます

テキストの判定

■アプリ

<View>
   <Text>プランの登録はありません</Text>
</View>

■テストコード

await expect(element(by.label("プランの登録はありません"))).toBeVisible();

ボタンをタップする

■アプリ

<TouchableOpacity onPress={this.props.onCreate} testID="addSchedule">
   <Ionicons name="ios-add-circle" size={80} color="#4DB6AC" />
</TouchableOpacity>

■テストコード

await element(by.id("addSchedule")).tap();

テキストを入力する

■アプリ

<TextInput
    placeholder="タイトルを入力"
    placeholderTextColor="#ffffff"
    onChangeText={title => this.setState({ title })}
    defaultValue={this.props.title}
    testID="inputTextScheduleDetailTitle"
/>

■テストコード

await element(by.id("inputTextScheduleDetailTitle")).replaceText("新宿駅");

大体、これくらい覚えておけばOK

実装

スケジュール登録を一通り操作してスクリーンショットを取る形式で実装したら、こんな感じになった

https://github.com/wheatandcat/Shiori/blob/master/ShioriNative/e2e/firstTest.spec.js

■ShioriNative/e2e/firstTest.spec.js

const { reloadApp } = require("detox-expo-helpers");
const { takeScreenshot } = require("./helpers");

describe("Example", () => {
  beforeEach(async () => {
    await reloadApp();
  });

  afterEach(async () => {
    takeScreenshot();
  });

  it("初期表示", async () => {
    await expect(element(by.label("プランの登録はありません"))).toBeVisible();
  });

  it("スケジュール追加", async () => {
    await element(by.id("addSchedule")).tap();
    await element(by.id("inputTextTitle")).tap();
    await element(by.id("inputTextTitle")).replaceText("葛西臨海公園");
    await element(by.id("completion")).tap();
    takeScreenshot();

    await element(by.id("addScheduleDetail")).tap();
    takeScreenshot();
    await element(by.id("inputTextScheduleDetailTitle")).tap();
    await element(by.id("inputTextScheduleDetailTitle")).replaceText("新宿駅");
    await element(by.id("inputTextScheduleDetailMemo")).tap();
    await element(by.id("inputTextScheduleDetailMemo")).replaceText(
      "8:00に西口に集合する"
    );
    takeScreenshot();
    await element(by.id("saveScheduleDetail")).tap();

    await element(by.id("addScheduleDetail")).tap();
    await element(by.id("inputTextScheduleDetailTitle")).tap();
    await element(by.id("inputTextScheduleDetailTitle")).replaceText(
      "葛西臨海公園"
    );
    await element(by.id("inputTextScheduleDetailMemo")).tap();
    await element(by.id("inputTextScheduleDetailMemo")).replaceText(
      "行く場所:砂浜、観覧車、水族園"
    );
    await element(by.id("saveScheduleDetail")).tap();

    await element(by.id("addScheduleDetail")).tap();
    await element(by.id("inputTextScheduleDetailTitle")).tap();
    await element(by.id("inputTextScheduleDetailTitle")).replaceText(
      "葛西臨海公園水上バス"
    );
    await element(by.id("saveScheduleDetail")).tap();

    await element(by.id("addScheduleDetail")).tap();
    await element(by.id("inputTextScheduleDetailTitle")).tap();
    await element(by.id("inputTextScheduleDetailTitle")).replaceText(
      "浅草寺二天門前"
    );
    await element(by.id("saveScheduleDetail")).tap();
    takeScreenshot();

    await element(by.id("saveSchedule")).tap();
  });
});

e2e実行

以下のコマンドで実行される

npm start
npmrun e2e

こんな感じで動作します

f:id:wheatandcat:20181217015240g:plain

実装してみた感想

ポジティブ

  • 予想以上に手頃に実装できる
  • 「testID」はキモいと思っていたが、完全にテスト用と割り切ってしまえば意外と気にならない
  • スタイル崩れチェックぐらいならスクリーンショットのチェックだけでもOKかも

ネガティブ

  • testIDに対応していないコンポーネントの対応ができない。ActionSheetの操作が出来ないのが痛い
  • expoだとandroidで実行ができない

ciで実行できるのかな?その辺が、まだよく分かってなので、もう少し調べる予定