ReactNativeテストまとめ

ReactNativeのテスト方法について備忘録としてまとめます。

テスト概要

使用パッケージについて

  • jest : テスティングフレームワーク
  • eslint: 静的解析
  • react-test-renderer: コンポーネントテスト
  • react-native-testing-library: コンポーネントテストユーティリティ
  • detox : E2Eテスト

テストの種類と割合について

ReactNativeを開発するに当たりテストの種類は主に

・静的解析 ・ユニットテスト ・統合テスト ・コンポーネントテスト ・E2Eテスト

の5種類が考えられる。

またこれらのプロジェクトにおけるテストコードの比率は このブログに紹介されているように

ユニットテスト > 統合テスト > E2Eテスト

が望ましいと考える。

これはなぜかと言うとユニットテストほどテスト環境を構築するのが容易で且つ実行時間が短いため 容易にテストサイクルを回すことができるためで、一方E2Eテストでは環境構築に時間がかかり且つ実行時間も遅い。

ではなぜすべてユニットテストにしないかと言うと、ユニットテストでは本番環境をすべて再現しているわけではなく あくまで仮想的な環境を作成しているため、テストコードが通っているからと言って本番環境で正常に動作するとは限らないためである。 一方でE2Eテストでは最も本番環境に近い状況でテストをしているため、本番環境で発生しうる問題を見つけるのに秀でている。 よってテストコードを書く上で上記の比率を意識することが重要であり、基本的にユニットテスト・統合テストで機能のテストをカバーし、本番環境では絶対に問題を発生させたくないコアな機能を中心的にE2Eテストを書くことが重要である。

静的解析

ReactNativeの静的解析では

  • ESLint
  • TCS を使用する。

ESLintではTypeScript(JavaScript)のフォーマットが規則正しく書かれているか確認し、 TCSではTypeScriptで書かれた型付けが正しく書かれているかを確認する。

それぞれの設定ファイルは

.eslint.jstsconfig.json に書かれているため参照してほしい。

また ESLintでは 拡張プラグインとして人気な eslint-config-airbnb を使用する。このプラグインを使用することで AirBnbで使用されている ESLintのルールを適用することができる。

コンポーネントテスト

コンポーネントテストではコンポーネントがどのようにレンダリングされ、 どのようにユーザーと対話をするかについて動作を保証させる。

  • ユーザーとの対話: ユーザーからの入力に対し正しい振る舞いをしているかどうかを保証する
  • レンダリング: コンポーネントの見た目が正しくレンダリングされているかを保証する

コンポーネントテストではnode上に仮想的なUIを実現している、つまり100% JavaScriptで書かれた環境となるため 実際に使用されるネイティブのUIでバグが発生していても気づくことができないため注意が必要である。

コンポーネントテストを行うためにいくつか便利なライブラリーが存在するため、これらを紹介する。

Test Renderer

Test Renderer とはDOMはネイティブ環境を使わずにReactコンポーネントを純粋なJavaScriptオブジェクトとして描画を可能にするライブラリー。

ブラウザーやjsdomを使用せずに、コンポーネントのView階層をスナップショットとして保存することができる。

例:

import TestRenderer from 'react-test-renderer'; function Link(props) { return <a href={props.page}>{props.children}</a>; } const testRenderer = TestRenderer.create( <Link page="https://www.facebook.com/">Facebook</Link> ); console.log(testRenderer.toJSON()); // { type: 'a', // props: { href: 'https://www.facebook.com/' }, // children: [ 'Facebook' ] }

Jestのスナップショット機能を使用してこのスナップショットをJsonとして保存し、スタイルなどの要素が変化していないかを確認することができる。Jestのスナップショットについて

また TestRenderer.create のアウトプットから指定したノードを検索することもできる。

import TestRenderer from 'react-test-renderer'; function MyComponent() { return ( <div> <SubComponent foo="bar" /> <p className="my">Hello</p> </div> ) } function SubComponent() { return ( <p className="sub">Sub</p> ); } const testRenderer = TestRenderer.create(<MyComponent />); const testInstance = testRenderer.root; expect(testInstance.findByType(SubComponent).props.foo).toBe('bar'); expect(testInstance.findByProps({className: "sub"}).children).toEqual(['Sub']);

また、コンポーネント内で ref を使用している場合、下記のように書くことでモック化することもできる。

import TestRenderer from 'react-test-renderer'; class MyComponent extends React.Component { constructor(props) { super(props); this.input = null; } componentDidMount() { this.input.focus(); } render() { return <input type="text" ref={el => this.input = el} /> } } let focused = false; TestRenderer.create( <MyComponent />, { createNodeMock: (element) => { if (element.type === 'input') { // mock a focus function return { focus: () => { focused = true; } }; } return null; } } ); expect(focused).toBe(true);

React Native Testing Library

React Native Testing Library とはReactNativeコンポーネントをより簡単にテストするため軽量なユーティリティである。 このユーティリティは react-test-renderer を使用している。

renderメソッドはまとめる

複数のrenderメソッドを it ごとに書くことは冗長であるため、 setup 関数を作成し renderメソッドをまとめる。

describe('FormTitle', () => { const titleValue = 'TEST TITLE'; function setup() { const utils = render(<FormTitle text={titleValue} />); const target = utils.container.firstChild; return { ...utils, target, }; } it('should render text', () => { const { getByText } = setup(); expect(getByText(titleValue)).toBeTruthy(); }); it('should be previous snapshot.', () => { const { asFragment } = setup(); expect(asFragment()).toMatchSnapshot(); }); });

E2Eテストについて

E2Eテストではdetoxを使用する。

今までのテストではあくまで仮想的な環境を作りテストを行ってきたが、 E2Eテストでは最も本番環境に近い状態でテストを行う。

そのためテストを行うまでの設定が非常に複雑かつ長くなるため、このテストでは本当に必要な機能のみに絞り テストを行ったほうが良い。

detoxを実行するためにapplesimutilsをインストールする必要があるため下記コマンドを実行する必要がある。

brew tap wix/brew brew install applesimutils

テストTIPS

テストコード配置

テストコードはE2E以外はコンポーネントディレクトリ配下に作成する。 このように配置することでテストコードと対象となるコードとの距離が近く、テストコードの見通しが良くなると考えられる。

例えば

Button/
├── index.tsx
└── style.ts

というButtonコンポーネントが存在していたとき

Button/
├── index.tsx
├── index.test.tsx
└── style.ts

と同じ Button ディレクトリ配下に index.test.tsx というテストファイルを作成する。

E2Eの場合は e2e/tests/ ディレクトリ配下に配置する。 またファイルの拡張子は e2e.ts とすること。

e2e/
├──tests/
                 ├── login.e2e.ts

テストコードの書き方について

Testing Framework として使用する Jest にて提供されている describeit を使用し、テスト機能の分割を行う。

describe ではテストのグルーピングを行い、 it では期待する機能の対象を絞ることができる。

また、統一感を持たせるために第一引数で渡すメッセージは should で始まるようにすること。("it should" で ~すべきという文章で始められる)

describe("filterByTerm", () => { it("should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); }); });

この例では filterByTerm という関数についてテストを行っているため、まず全体を describe で囲みメッセージを対象となる関数名 filterByTerm としている。 そして it で filterByTerm で期待する挙動をまとめている。 この例では filterByTerm 関数では第2引数に含まれる文字を持つリンクを正常に返すかどうかをテストしている。

test vs it

itはtestのailias であるため機能上差異がないが

it を使うことで主語を固定する成約を与えることができるため、統一的に文言を揃えることができると考える。

また既に it をかけることから、幾分文章を簡潔に書くことができる。

たとえば

test('if it does this thing', () => {})

test を使う場合は主語を明示しなくては行けない場合が多いが it を使用することで

it('does this thing', () => {});

と簡潔に書くことができる。

セットアップ・クリーンアップ処理は切り出す

それぞれのテストケースで共通的に必要となる処理は beforeEach, afterEach でまとめることで、 コードを簡潔にし、保守性を高めることができる。

beforeEach(() => { // テストが走る前に呼び出される }); afterEach(() => { // テスト終了後に呼び出される });

ネットワーク通信はモック化をする

ネットワーク通信が関わる処理では、モックサーバーを作成するのではなく、 通信処理を行う関数のモック化を行うことでテスト実行速度を上げることができる。

const fakeUser = { name: "Joni Baez", age: "32", address: "123, Charming Avenue" }; jest.spyOn(global, "fetch").mockImplementation(() => Promise.resolve({ json: () => Promise.resolve(fakeUser) }) );

またテスト完了後にはモックを削除する。

global.fetch.mockRestore();

スナップショットについて

React Native Testing Libraryを使用しスナップショットテストを行うことができる。

it('should match snapshot', () => { const rendered = render(<ErrorDisplay value={'abacaba'} />).toJSON(); expect(rendered).toMatchSnapshot(); });

スナップショットのサイズを小さくする

スナップショットのサイズが大きくなるほどメンテナンス性が下がるため極力サイズを小さくする。 これに制約を加えるため、 no-large-snapshots を使用する。

スナップショットの更新

スナップショットの更新はスナップショットテストが意図してエラーになっているのか判断が難しいため慎重になるべきである。

エラーが発生したとき、もし対象となるテストコードが自分の変更範囲のものでない場合、 それが正常なのかを関係者に聞き jest --updateSnapshot で更新を行う。 その際にリーダーへ確認を取れるとより良い。

E2Eテストのコンポーネント指定にはtestIDを使用する

テスト対象となるコンポーネントには testId propを使用し、 testId を使用してコンポーネントを指定することが推奨されている。

ただし、すべてのReactコンポーネントがこのpropsに対応しておらず、ReactNativeで組み込まれている View, Text, TextInput, Switch, ScrollView は対応している。 もし独自のコンポーネントを作成する際はネイティブのViewへ渡すようにする必要がある。

コンポーネント例

<View> <TouchableOpacity testID='MyUniqueId123'> <Text>Some button</Text> </TouchableOpacity> </View>

テスト例

await element(by.id('MyUniqueId123')).tap();

参考

©Tsurutan. All Rights Reserved.