jest
Snapshot
reactnative

Test on React Native - 小さなチームではじめるテスト運用

私たちのチームでは今年の頭からチームでユニットテストを書き始めています。
極力シンプルな方針のみを取り決め、小さく始めました。
この記事では自分たちチームのテストの設計とつまづいてきた問題の解決方法を紹介します。

テスト設計

テスト設計としては、テストをする対象とその対象でなにを確認するべきかを明確にし、その優先度を決めました。

なにをテストするか

Components (重要度:A)
Action Creator (重要度:C)
Async Action Creator (重要度:B)
Reducer
- 引数をそのままプロパティにアサインしているパターン (重要度:C)
- 引数から別の値を算出してプロパティに値をアサインしているパターン(重要度:A)

※APIを叩かないActionCreatorに関しては、基本的にユーザーの入力を渡す以上の役割を持たせないこと。
ユーザーから受け取った値を加工したい場合は、reducerで行う。
※非同期処理の最後にdone()を追加するとPromiseを返さないのでテストが失敗するため、使わないようすること。

なにを確認するのか

Components
スタイルを変更した際に、スナップショットの更新により、自分が行った修正による差分が想定通りであることを確認する。
想定していない場所で変更があれば、修正により意図せぬ場所へ影響を及ぼしていることになる。
また、必要なプロパティが渡せていない場合にはテスト自体が落ちるので、クラッシュを未然に防ぐことができる。

Action Creator
想定するアクションが生成されているかを確認する。
Action CreatorはView側から送られてきた引数に変更を加えることなく、そのままアクションへ渡すことが望ましく、そのルールを守っていれば基本的にここに関するテストは必要なくなる。

Async Action Creator
想定するアクション群が生成されているかを確認する。
非同期のActionCreatorではAPIからの返答によっていくつかのActionCreatorを呼び出すことがある。
想定するActionCreatorがすべて走っていなければどこか途中のActionCreatorが失敗していることになる。
ActionCreator単体ではテストをあまり書かないがここでテストが失敗した時に限り、個々の非同期ではないActionCreatorを確認することになる。
よってバグが潜んでいる箇所を絞りこむことができる。

Reducer

  • 引数をそのままプロパティにアサインしているパターン
    アクションにより、ステートが想定通りに変更されていることを確認する。
    ここでテストが失敗したらどこかをtypeしているはず。

  • 引数から別の値を算出してプロパティに値をアサインしているパターン
    アクションにより、ステートが想定通りに変更されていることを確認する。
    しかしここにはロジックがあるのでロジックに応じたケース分テストが必要になる。

優先度

A: 新規開発時に極力あわせてテストを書くようにしましょう。
   また、既存のコードでテストが書いていない箇所を見つけた場合は積極的にテストを書いてください。
   重要度Aに関しては常に100%のカバレッジの維持を目指しましょう。
B: 複雑な内容だと感じる場合にはconsole.logで値を確認する代わりに、テストを書きましょう。
C: 優先順位は低いです。時間に余裕があれば書きましょう。

運用

最初はJestの公式サイトで紹介されているようにMatchers(Using Matchers · Jest)を使うテストを書いていましたが、現在ではSnapshot*を用いたテストが主流になりました。

Snaphotにより結果をJSON形式で出力し、初めてテストを書いた時にはその出力結果が期待通りであることを、2回目以降は差分のみを目で確認します。

Snapshotでテストを書くことのデメリットとしては、具体的になにを確認しているのかを第三者が確認できないこと、そしてテストする本人もそれを明記しないので確認が雑になりうる点です。

しかし、Snapshotは初めてテストを書いた時のSnapshotの確認は大変ですが、以降は差分を確認するだけですし、テストのコードもテスト用のメソッドをいくつも覚える必要はなく、修正をしてテストを更新するのもコマンド1つでできるのでMatchesを使うテストよりも総合的にはテストにかける工数が少なくてすんでいます。

describe('Avatar: ', () => {

  test('通常のアバター', () => {
    const props = {  
      icon: 'people',
      theme: defaultThemeValue,
      styles: createThemeStyle(defaultThemeValue),
    };

    const tree = renderer.create(<Avatar {...props} />);
    expect(tree).toMatchSnapshot();
  });

});

渡すプロパティを用意し、rendererでcreateする対象のコンポーネントを指定しなおすだけです。
モックデータをまとまったオブジェクトごとに用意しておけばプロパティを用意する手間もさらに効率化できます。

モックデータ
スクリーンショット 2018-04-11 16.47.06.png
モックデータ利用箇所
スクリーンショット 2018-04-11 16.52.00.png

Snapshot
Viewの状態をJSON形式で出力し、最初に撮ったsnapshotと比較して以降意図しないUI変更が行われていないことを機械的に確認することができる機能
Snapshot Testing · Jest

Tips

コンポーネントが備えるメソッドに紐づく処理を確認する

react-test-rendererのcreate()では実際のDOMを使いませんが、メモリーの中でコンポーネントをフルレンダリングし、返却されるインスタンスにはメソッドやプロパティも含まれています。
このインスタンスからコンポーネントクラスが備えるメソッドを実行することができます。
具体的には下記のようにcreate()で生成したインスタンスから getInstance() を使うことでメソッドへアクセスできます。

test('todayボタンを押下すると、アジェンダで今日を選択後、今日を基準にデータを取得しなおす', () => {
    const store = mockStore();
    const day = {
      dateString: '2018-02-01',
    };

    const props = {
      actions: {
        selectAgendaDay: () => store.dispatch(selectAgendaDay(day)),
        refreshEvents: () => store.dispatch(refreshEvents()),
      },
    };

    const component = renderer.create(<Agenda {...props} />);
    component.getInstance().onPressTodayBtn();

    expect(store.getActions()).toMatchSnapshot();
});

上記のテストはカレンダーのコンポーネントのテストの一部です。
”今日へ戻る”というボタンのonPressにセットされているonPressTodayBtn()が実行された時に想定するアクションが走っているかを確認しています。
この時onPressTodayBtn()をトリガーとしてはしるActionCreatorは用意しておく必要があります。

stateの変更を伴うテスト

stateの変更を行う場合にはEnzymeなどを用いてコンポーネントをshallowレンダリングすればsetStateをすることができます。
Enzymeを使わずにstateを変更させるには上記のgetInstance()を使用し、stateを変更するメソッド自体を呼び出すという方法があります。

test('todayボタン表示', () => {
    const props = {
      ...initialState,
      calendar: {
        agendaItems: {
          '2018-02-01': [],
        },
      },
      selected: '2018-02-01',
    };

    const component = renderer.create(<Agenda {...props} />);
    // todayボタンの表示に関するstateの値をtrueにする関数を実行
    component.getInstance().switchDisplayOfToday(true);

    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

時間

テストは常に同じ結果になるべきであるため実行した時間によって結果が異なる部分にはモックが必要です。

// 1: 出力結果は[MockFunction]
Date.now = jest.fn;

// 2 出力結果は "2018-02-09"
Date.now = jest.fn(() => '2018-02-09');

// 3 出力結果は "2018-01-10T00:00:00+0900"
Date.now = jest.fn(() => moment('2018-01-10T00:00:00+0900'));

momentTZ.tz.setDefault('Asia/Tokyo');

このモックで気をつけないといけないのはCircle CIなどでテストを自動化している場合にCircleCIのテスト実行環境のタイムゾーンによってテストの結果が異なる場合があります。

なので時刻を固定するテストではタイムゾーンの固定処理momentTZ.tz.setDefault('Asia/Tokyo'); も一緒に書きます。
Moment Timezone | Home