ReactNativeでE2EテストをするDetoxをちょっとだけ触ってみた(iOS)

Detoxというライブラリを使ってE2Eテストを書いてみる

github.com

前提

準備

シミュレーターを動かすのに必要なのでインストールする

$ brew tap facebook/fb
$ export CODE_SIGNING_REQUIRED=NO && brew install fbsimctl
$ brew tap wix/brew
$ brew install --HEAD applesimutils
$ npm install -g detox-cli

テストしたいプロジェクトに導入する

$ npm install detox mocha --save-dev

初期化コマンドを実行すと

./e2e フォルダが生成されます

続いてビルドコマンドで成功すれば準備完了

$ detox init
$ detox build

動かす

$ detox test を実行すると ./e2e/xxx.spec.js ファイルが検出されてテストされる

適当な画面を書いて動かしてみた

テストを実行すると、画面が勝手に動き出して

書いたとおりにUIが変更されるかチェックされる

f:id:rskull:20170815182823g:plain

デモ: GitHub - rskull/detoxDemo

こんな感じのコード

describe('Example', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
  });
  
  it('テキスト入力', async () => {
    await expect(element(by.id('welcome'))).toBeVisible();
    await element(by.id('textInput')).typeText('Hello!');
    await element(by.id('textInput')).clearText();
    await element(by.id('scrollview')).tap();
  });

  it('スクロールしてメニュー開閉', async () => {
    await element(by.id('scrollview')).scrollTo('bottom');
    await element(by.id('menuButton')).tap();
    await expect(element(by.id('menu'))).toBeVisible();
    await element(by.id('menuCloseButton')).tap();
    await expect(element(by.id('menu'))).toNotExist();
  });
})

操作対象とのヒモ付は testID を付ける

対応してるのは View, Text, TextInput, Switch, ScrollView で、ラップしてる場合は自分で渡せるようにしないといけない

例えば

<View testID='welcome'>
  <Text>Hello</Text>
<View>
describe('Example', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
  });
  it('初期画面が表示されてる', async () => {
    await expect(element(by.id('welcome'))).toBeVisible();
  });
})

また、要素に対してタップやスクロールというような操作も実行できる

<TouchableOpacity testID='button' onPress={() => {}}>
  <Text>Touch</Text>
</TouchableOpacity>
  it('sample', async () => {
    await element(by.id('button')).tap() // onPressが実行される
  });

タップ以外の操作もだいたい出来る

  • .tap()
  • .longPress()
  • .multiTap()
  • .tapAtPoint()
  • .typeText()
  • .replaceText()
  • .clearText()
  • .scroll()
  • .scrollTo()
  • .swipe()

詳しくはドキュメントへ: https://github.com/wix/detox/blob/master/docs/APIRef.ActionsOnElement.md#methods

所感

  • testID 埋め込むのがちょっとめんどくさいのと、書き方によって埋め込みが難しいところもある
  • OSSのアプリに組み込んでみたらビルドエラーで動かなかったり、まだ安定してない感があった
  • 動いてる様子を見るのが楽しかった(たぶん最初だけ)
  • 使うとしたらコアで操作の多い重要な部分だけ書くのが良さそう
  • Androidはまだサポートしてないみたい

参考リンク

バージョン指定でReactNativeのプロジェクトを始める

メモ

$ react-native init App --version v0.46.0

運用してるプロジェクトで入れたモジュールがうまく動いてない時

同じバージョンの新規プロジェクトを作って一番素の状態で想定通り動くかテストしたい時に使う

わりと最終手段だけど、問題の切り分けできて良い

ReactNativeで開発中、Androidのビルドで64K問題のエラーが出てハマった

64K問題

ReactNativeに限った話ではないが

Androidアプリでメソッド数が65536を超えるとビルドエラーになる問題のこと

Androidエンジニアにとっては常識だと思うが、Webから入った人間からすると結構厄介だった

検索すると沢山ヒットする

分かりやすかった記事

dev.classmethod.jp

こんなエラーが出る

f:id:rskull:20170731233923p:plain

調査する

前提として64K以上いけるMultidex化はしたくない

ビルドしたapkファイルにいくつメソッドがあるか調べる

dex-method-counts を使う方法があるけど、Android Studioの標準機能で出来る

これ64Kでビルド落ちた時apk作られないから調べられなくない…?

とりあえず、ビルド通ってた時期のapkファイルを調べてみた

f:id:rskull:20170731235542p:plain

めっちゃギリギリ! このあと新しい機能を入れたら落ちるようになったので、まずは原因を調査

$ ./gradlew :app:dependencies

を実行すると依存してるパッケージの一覧が出る

64K問題の原因で多いのは google-services 系を全パッケージ読み込んでるとか、別々のバージョンが入ってるとからしい

medium.com

compile (project(':react-native-device-info')){
    exclude group: "com.google.android.gms" 
}
compile ("com.google.android.gms:play-services-gcm:10.0.1") {
    force = true;
}

exclude するとコンパイル対象から除外されるので、依存してるreactnativeのパッケージからすべて除外して 最後に除外した play-services-xxx を必要なもののみ追加する

ここら辺の設定は人による

いろいろやってメソッド数が減ったのは確認できたけど、目的の機能入れたときに結局落ちた

中途半端になってしまったが調査中‥

いい方法を知りたい

TwitterからエゴサーチしてSlackやChatworkに投げる

検索したツイートを定期的にSlackに投稿はIFFFTで出来るけど、Chatworkに投稿はZapierという類似サービスくらいしかなかった

一月の制限が割りときつくて途中で止まっちゃので

Goの勉強がてら自分で作った

github.com

まだ作法も全然わからないし

まだテスト書いてないし

READMEも適当

(まだChatworkしか対応してない)

これから改善していく

ReactNativeでActionSheetを実装する - その②

この前書いたActionSheetの記事書いたけど、特定の状況下だと使えないじゃん!ってなることに気づいた

rskull.hateblo.jp

駄目だった理由

iOSはネイティブの機能を使ってるのでどこで呼び出しても起動するけど

Androidはネイティブの機能ではなく、モーダルっぽいのをRN側で実装されてるので

ヘッダーのアイコンを押したときに起動する、みたいなことをすると

モーダルの位置が正しく計算されず、変なところに表示されたりする。

@expo/react-native-action-sheet

<ActionSheetProvider>
  <ActionSheet />
</ActionSheetProvider>

このコンポーネント自身がActionSheetを起動するためのボタンを持っているのと

Androidではポップアップを持っているので、置き場所によって正常な表示ができないことがある

駄目じゃん…

起動するためのボタンと、ポップアップのコンポーネントは場所を分けたい

近いことをしてたのがコレ

https://github.com/beefe/react-native-actionsheet

<ActionSheet
  ref={o => this.ActionSheet = o}     
/>
showActionSheet() {
  this.ActionSheet.show()
}

自身の ref を渡して起動する

コレだとトリガーを好きなところにおけるけど、Android側のUIが完全にiOSで好みじゃない

結局いいかんじだったやつ

github.com

Androidはネイティブに組み込む必要あってちょっとめんどくさいけど

RNの最新バージョンでも動作してたし大丈夫そう

何がいいってネイティブ実装だし、完全に ActionSheetIOSAPIと一緒なところが良い

どこでも呼び出せて、UIもAndroidに合ったものになってる

インストー

READMEの通り

$ npm install @yfuks/react-native-action-sheet@latest --save
$ react-native link @yfuks/react-native-action-sheet

あとはビルドして終わり

使い方は ActionSheetIOS と同じ

import ActionSheet from '@yfuks/react-native-action-sheet';

...

onOpenActionSheet() {
  const options = ['Delete', 'Save', 'Cancel'];
  const destructiveButtonIndex = 0;
  const cancelButtonIndex = 2;
  ActionSheet.showActionSheetWithOptions({
    options,
    destructiveButtonIndex,
    cancelButtonIndex
  },
  buttonIndex => {
    console.log(buttonIndex)
  })
}

デモ

iOS

f:id:rskull:20170704015625g:plain

Android

f:id:rskull:20170704015649g:plain

満足

ReactNativeにActionSheetを実装してみた

ActionSheetとは?

この下から出てきてるやつ

f:id:rskull:20170628001833p:plain

RN本体でサポートしてくれてはいるが、iOSの標準機能なので iOS用のコンポーネントになってる。

ActionSheetIOS

うーん…同じコードで両方対応したいですね…

react-native-action-sheet

そんなときはコレ

github.com

いい感じにiOSAndroid両方対応してた

Androidは(多分)見慣れたポップアップみたいな感じで表示される

expoでサンプルコードが公開されてるので、実機とアプリがあれば簡単に確認できて便利

※ expoについては省略

今回はexpoじゃなくて、通常通り react-native init app の流れからの組み込みを想定してる

組み込む

まずはプロジェクトのルートでパッケージをインストールする

$ npm install @expo/react-native-action-sheet

サンプルコードをほぼそのまま使って動くかやってみると

import React, { Component } from 'react';

import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import {
  ActionSheetProvider,
  connectActionSheet,
} from '@expo/react-native-action-sheet';

class app extends React.Component {
  render() {
    return (
      <View style={{ flex: 1 }}>
        <ActionSheetProvider>
          <ActionSheet />
        </ActionSheetProvider>
      </View>
    );
  }
}

@connectActionSheet class ActionSheet extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <View style={{ marginBottom: 30 }}>
          <Text style={{ textAlign: 'center' }}>
            Hello! This is a simple example app to demonstrate
            @exponent/react-native-action-sheet.
          </Text>
        </View>

        <TouchableOpacity
          style={{ backgroundColor: "#000" }}
          onPress={this._onOpenActionSheet}
        >
          <Text style={{ fontSize: 15, color: '#fff' }}>Open action sheet</Text>
        </TouchableOpacity>
      </View>
    );
  }

  _onOpenActionSheet = () => {
    // Same interface as https://facebook.github.io/react-native/docs/actionsheetios.html
    let options = ['Delete', 'Save', 'Cancel'];
    let destructiveButtonIndex = 0;
    let cancelButtonIndex = 2;
    this.props.showActionSheetWithOptions(
      {
        options,
        cancelButtonIndex,
        destructiveButtonIndex,
      },
      buttonIndex => {
        // Do something here depending on the button index selected
      }
    );
  };
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

デフォルトだと、ES2016のDecoratorがBabelで有効になってないのでエラーがでます

> 23 | @connectActionSheet class App extends React.Component {
     | ^
  24 |   render() {
  25 |     return (
  26 |       <View style={styles.container}>

こんな感じのエラー

Decoratorを使えるようにする

プラグインを入れて

$ npm install --save babel-plugin-transform-decorators-legacy

.babelrc に設定を付け加える

{
  "presets": ["react-native"],
  "plugins": [
    "transform-decorators-legacy"
  ]
}

参考:Using ES2016 Decorators in React Native - Modus Create

npmインストールとかしたらプロンプトで動いてるやつ再起動してください Ctrl+Cで止めてプロジェクトルートで $ react-native start すれば動き出します

動かしてみる

iOS

f:id:rskull:20170627235432g:plain

Android

f:id:rskull:20170628000432g:plain

なんかDeprecated警告出てたけど、そのうち修正してくれるだろう…

Flwotypeに対応

Flowtype使ってるひとはチェック引っかかるので .flwoconfig に以下の内容を設定すれば大丈夫です

[options]
esproposal.decorators=ignore

参考:ES7 decorator support or a way to ignore the error · Issue #606 · facebook/flow · GitHub

感想

いい感じ ٩( ‘ω’ )و

※ 追記

特定の状況下だと使えないことがあった

rskull.hateblo.jp

新しいMBPにXcodeの開発環境を移行したとき証明書周りでハマった

タイトルの通り。

開発ビルドとリリースビルドの証明書がうまいこと設定できない問題はよくあるみたい。

いくつかハマった点をメモする

中間証明書がなかった

【注意】Apple Worldwide Developer Relations中間証明書の期限切れ - Qiita

まずはAppleから中間証明書というものを落として登録する必要がある。

そういえば昔そんな作業したなーって感じだった。

旧開発機から証明書をインポート

このあたりを参考にした

トラブルシューティング

Mobile Dev. (iPhone + Objcetive C) » 2台目以降のMacで実機テストをする方法

旧開発機のKeychain AccessからExportして.p12ファイルを作って それを共有するだけでOKだった。

プッシュ通知のための証明書を作成する過程でも色々ファイルを作成するので

どれが必要でどれがなんの役割なのかごっちゃになってたけど、今回の作業でスッキリした。

旧開発機からのインポートが無理な場合(ざっと)

手元の本番証明書が、MBP移行のタイミングでちょうど期限切れしてて、問題に気づくのが遅れた…

けっきょく証明書を作り直す必要がりそう。

  • 証明書の要求ファイル(CSR)を作成
  • DeveloperコンソールのCertificationsで追加ボタンを押す
  • 必要な種類を選んで先程作ったCSRをアップして証明書を発行
  • ダウンロードした証明書をクリックして追加
  • プロビジョニングプロファイルを編集して新しく追加されたCertificationsを選択して生成
  • ダウンロードして取り込む
  • これでXcodeみたら有効になってた

最初に証明書を作る時と同じような手順です。

ついでに期限が切れそうだった本番証明書も更新した。

もしかして普通一緒に更新するもの?

とりあえず問題なく更新して環境も移行できました。

iOSプッシュ通知用証明書の更新方法 - Qiita

細かい部分は探せばだいたい情報はあるから省く