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で実行できるのかな?その辺が、まだよく分かってなので、もう少し調べる予定

react-nativeでsortableなUIを実装

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

webならドラッグ & ドロップでアイテム順番を入れ替えるけど、react-nativeならどうするのかなー と思いググったらreact-nativeでsortable listを実装しているライブラリがあったので、今回はそのライブラリを紹介します

sortableとは

js.studio-kingdom.com

ドラッグ & ドロップで要素を入れ替えるUIを指します。 順番入れ替えを実装する際に利用されるUIで、直感的な操作を提供できます

react-nativeでの実装

調べるといくつかライブラリが出てきますが、今回は以下のライブラリで実装してみた

github.com

選定理由は以下の通り

実装してみた

現状、こんな感じ

f:id:wheatandcat:20181212094406g:plain

並び替えに行くまでの導線が長いので、まだまだ改善の余地はあるけど、実装したかったことはできたので一旦良しとしよう

コード

ほぼExampleのコピペですが、大体こんな感じで動くコードが書けた。

https://github.com/wheatandcat/Shiori/blob/master/ShioriNative/src/components/organisms/SortableSchedule

■ Cards.tsx

import React, { Component } from "react";
import SortableList from "react-native-sortable-list";
import getKind from "../../../lib/getKind";
import Card from "../../molecules/Schedule/Card";
import Row from "./Row";

type DataKey = string | number;

export interface ItemProps {
  id: string;
  title: string;
  moveMinutes: number | null;
}

export interface Props {
  data: ItemProps[];
  onChange: (data: any) => void;
}

export interface RowProps {
  data: ItemProps;
  active: boolean;
}

export default class extends Component<Props> {
  renderItem({ data, active }: { data: ItemProps; active: boolean }) {
    return (
      <Row active={active}>
        <Card id={data.id} title={data.title} kind={getKind(data.title)} />
      </Row>
    );
  }

  onChange = (nextOrder: DataKey[]) => {
    const data = nextOrder.map(id => {
      return this.props.data.find(item => Number(item.id) === Number(id));
    });

    this.props.onChange(data);
  };

  render() {
    const obj = this.props.data.reduce((o, c) => ({ ...o, [c.id]: c }), {});

    return (
      <SortableList
        data={obj}
        renderRow={this.renderItem.bind(this)}
        style={{ flex: 1 }}
        onChangeOrder={this.onChange}
      />
    );
  }
}

■ Row.tsx

import React, { Component } from "react";
import { Animated, Easing, Platform, View } from "react-native";

export interface RowProps {
  active: boolean;
  children: any;
}

export default class extends Component<RowProps> {
  _active: any;
  _style: any;

  constructor(props: RowProps) {
    super(props);

    this._active = new Animated.Value(0);

    this._style = {
      ...Platform.select({
        ios: {
          transform: [
            {
              scale: this._active.interpolate({
                inputRange: [0, 1],
                outputRange: [1, 1.1]
              })
            }
          ],
          shadowRadius: this._active.interpolate({
            inputRange: [0, 1],
            outputRange: [2, 10]
          })
        },

        android: {
          transform: [
            {
              scale: this._active.interpolate({
                inputRange: [0, 1],
                outputRange: [1, 1.07]
              })
            }
          ],
          elevation: this._active.interpolate({
            inputRange: [0, 1],
            outputRange: [2, 6]
          })
        }
      })
    };
  }

  componentDidUpdate(prevProps: RowProps) {
    if (this.props.active !== prevProps.active) {
      Animated.timing(this._active, {
        duration: 300,
        easing: Easing.bounce,
        toValue: Number(this.props.active)
      }).start();
    }
  }

  render() {
    return (
      <Animated.View style={[this._style]}>
        <View style={{ paddingBottom: 50 }}>{this.props.children}</View>
      </Animated.View>
    );
  }
}

最後に

ドラッグ & ドロップはreact-dndという魔境ライブラリの思い出があったので手を出していなかったけど、「react-native-sortable-list」は使い方が限定されているので、実装しやすかった。

github.com

開発者視点で一年間Atomic Designと付き合ってみた結果

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

ここ一年くらい複数のプロジェクトでAtomic Designを取り入れてやっていたので、その辺の話を開発者視点で書いていこうと思います。

Atomic Designとは

bradfrost.com

解説については↑のサイトを参照

コンポーネントの種類

Atomic Designでは以下の種類のコンポーネントに分けて実装

  • Atoms
  • Molecules
  • Organisms
  • Templates
  • Pages

個人開発プロジェクトで採用しているので実装例を元に各種類について解説します

Atoms

最小単位のパーツコンポーネント 「テキスト」、「画像」、「テキスト入力」、「チェックボックス」、「セレクトボックス」等々が該当します

f:id:wheatandcat:20181203012436p:plain:w400

Molecules

Atomsを2つ以上組み合わせたコンポーネント

f:id:wheatandcat:20181203013444p:plain:w300

f:id:wheatandcat:20181203013459p:plain:w300

f:id:wheatandcat:20181203013516p:plain:w300

Organisms

MoleculeとAtomsを組み合わせて1機能として自己完結しているコンポーネント

f:id:wheatandcat:20181203013838p:plain:w300

MoleculeとOrganismsの違いについては下記の記事を読むと分かりやすいです。 frasco.io

Templates

Organisms、Molecule、Atomsを組み合わた1画面のコンポーネント

f:id:wheatandcat:20181203015600p:plain:w300

Pages

Templatesに実際の情報を組み込んだ最終的なコンポーネント

f:id:wheatandcat:20181203015106p:plain:w300

開発での実装

採用しているプロジェクト情報

github.com

  • react-native (expo)で実装

フォルダ構造

  • src/components/ 以下に、「atoms」〜「 pages」のフォルダを作成し該当するコンポーネントを作成している

f:id:wheatandcat:20181203020438p:plain:w700

Shiori/ShioriNative/src/components at master · wheatandcat/Shiori · GitHub

storybookとの連携

storybookはコンポーネント単位の表示を管理ツールできるツールです storybookの詳しい情報は下記参照

github.com

Atomic Designに合わせてstorybookファイルを作成しコンポーネントを管理しています

f:id:wheatandcat:20181203021936p:plain:w700

Atomic Designを採用してみての話

明確なメリット

問題点

理解力の差によって配置されるべきコンポーネントに差異が起きる

  • 上記にもリンクを貼っているがMoleculeOrganismsは特に誤った配置がされやすいので注意が必要

十分に注意してコード修正、レビューを行わないと負債になりやすい

  • リファクタリングの際にコンポーネントの役割が「Atoms→Molecule」や「Molecule→Organisms」に変更されたが、フォルダの移動がされずに放置され負債になるケースが起きやすい
  • 上記の負債はあくまで「Atomic Design」での誤りなので、もちろんテストコードでは分からず、またコードレビューのみでは分かりづらい。なので定期的なstorybookでのチェック and リファクタリングコストをかける必要がある

Atomsにするか個々のコンポーネントに書いてしまうか悩む

  • 複数箇所で使用されるコンポーネントならAtomsで定義する価値があるけど、将来的にどの程度使われるか分からないコンポーネントをAtomsでわざわざ定義する必要があるのか悩む

まとめ

  • 一年間かけて徐々に理解力が深まっている感じはする
  • 問題点はあるが、それでも何も基準無しに作るのよりは大きくマシだと言えると思う
  • コードレビューだけだと間違いを見逃すからstorybookは必須だと思う(もちろん、理想は自動テストだけど。。。)

実際のコンポーネントを確認したい人は

  • 以下でstorybookのアプリを公開しているのでexpoからご確認ください 🙇

expo.io

もろもろの参考URL

storybook 4.0のReact Nativeに神アップデートきた!

詳しい更新内容は↓参照

medium.com

React Native関係のアップデート内容

ついに、サーバー無しでアプリのみでstorybookができるようになったので、早速ためしてみました

github.com

パッケージをアップデートしただけでは動かなかったが、チュートリアルを見てちょっと書き直したら動きました。

https://storybook.js.org/basics/guide-react-native/

アプリで表示

f:id:wheatandcat:20181102015807p:plain:w300

f:id:wheatandcat:20181102015828p:plain:w300

f:id:wheatandcat:20181102015609p:plain:w300

アプリのみでstorybookの機能が成立している!感激

storybookをpublishする

アプリだけで動くようになったのでstorybookをアプリとしてpublishすることができます。

expoなら、nameとslugを変更したapp.jsonを作成してcliで指定するだけでstorybook用のアプリが配信できます。

expo-cli publish --config storybook/app.json

配信したstorybookアプリ

expo.io

通常の配信アプリ

expo.io

長らくreact-nativeのstorybookは不遇だったけど、ここに来て一気に神ツールになった

(日記)アプリ作成進捗①

進捗

  • デザインは観覧と入力まで完了

marvelapp.com

expo

一応デザイン分まではモック画面作成完了

expo.io

久々にexpoを使ったらコンソールがブラウザになってて驚いた。使いやすい

f:id:wheatandcat:20181021014001p:plain

今ところのスクショ

f:id:wheatandcat:20181021014710j:plain:w300

f:id:wheatandcat:20181021014737j:plain:w300

f:id:wheatandcat:20181021014811j:plain:w300

f:id:wheatandcat:20181021014841j:plain:w300

f:id:wheatandcat:20181021014903j:plain:w300

趣味プロダクト管理ツール紹介

アプリ作り始めたので、色々プロダクト管理お試し中

タスク管理

asana

asana.com

シンプルなタスク管理ツール こんな感じで使っている。jiraとかよりシンプルで、Todoリストよりも残タスクが見やすくていい感じ

f:id:wheatandcat:20181016041710p:plain

アイディア管理

参考画像とか、思っていたことが 雑にまとめられるツールが欲しいと思い試し中

milanote

www.milanote.com

画像とかテキストをまとめられるツール。 以外と、参考画像をぺたぺた貼っておくのは便利

f:id:wheatandcat:20181016042542p:plain

Mandal-Art

www.mandal-art.com

中心にあるのがメインテーマを書いて、周囲やりたいを書いていくみたいなヤツ。 現状は、最初にやるべきことがぶれないように管理するツールとして運用している感じ。

デザイン管理

Marvel app

marvelapp.com

シンプルなデザイン作成ツール。 ワイヤフレーム作成として使っている。 ガチでやるなら、sketchとかfigmaを使ったほうが良いと思います。

f:id:wheatandcat:20181016044004p:plain

情報管理

ここが今一番悩んでいるツール、今は以下の2つを試し中

Dropbox Paper

www.dropbox.com

Dropboxが提供しているドキュメント共有サービス。 簡単に編集できて、markdownを使える。ただプロジェクト毎とかの管理方法が無いので微妙

Scrapbox

scrapbox.io

こっちは、プロジェクト毎とか、記事をサムネ表示できるとか、よく出来ているサービス。 ただmarkdownに対応してないので、イマイチ本腰入れて使えていない

(日記)最近アプリ作り始めた

昨日から作り始めた

github.com

簡単なアプリの想定だから年内に公開を狙って行こうと思います。

採用する予定技術

バックエンド

アプリ

  • react-native
  • expo

Web版

  • nuxt

その他

  • データ保存 => datastore
  • 認証 => Auth0

くらいで軽く作る予定。検索系は難しいけど、ほぼただで使えるのが datastoreの良いところ。Cloud Spannerはもちろん、Cloud sqlも完全趣味で使うには意外と良い値段するから辛いところ