Reduxと自動テスト
さて、ReactとReduxで作られたアプリをテストしたいと思ったときに、どんなところをテストすると費用対効果が高いでしょうか。次のような機能はテストしづらいと筆者は考えています。
- UI
- 外部との通信を含む処理
UIは見た目について扱うため、テストがしづらい分野です。また、自動テストは常に同じ条件の下で同じ結果が出ることを確認するための仕組みである以上、常に同じ条件であることが保証できない外部との通信が含まれる処理は、自動テストを実施しづらいものになります。本来、JestはReactのUIテストを自動化する点では他のテスティングフレームワークに比べて秀でていますが、関数のテストに比べると実用的な運用をするための難易度が高いため、本記事では扱いません。また、非同期処理をテストするためのツールについてもJestはいくつかの解決策を用意していますが、これも1つ上の難易度になるため、本記事では扱いません。
さて、そういった事情を鑑みると、UIであるReactや、外部との通信が含まれることが多いAction Creatorのテストは少し難しいということになります。では、Reducerはどうでしょうか。Reducerの役割は「現在の状態とActionを与えられると、新しい状態を返す関数」でした。関数の中で同期的に実行された計算結果のみが返却され、非同期な処理が計算結果に影響することはありません。これは自動テストと相性が良さそうです。
ここからはReduxの中で最もテストしやすいモジュールである、Reducerのテストについて扱います。
Reducerをテストする
それでは、Reducerをテストする場合の考え方を紹介します。基本的にはどんな状態のときにどんなActionを与えたら、どんな状態になるのかを検証していく方針を取ります。
第8回で題材にした、カウンターアプリのReducerについて考えてみます。カウンターアプリのReducerは、リスト7のような実装になっていました。
count defaultState = { count: 0 }; const counter = (state = defaultState, action) => { switch(action.type) { case 'INCREMENT': return { count: state.count + action.payload.amount }; case 'DECREMENT': return { count: state.count - action.payload.amount }; case 'RESET': return { ...defaultState } default: return state; } }; export default counter;
これに対するテストコードは、リスト8の形になりました。
import reducer from "./counter"; describe("type: INCREMENT", () => { test("amountが1のときにcountが1増える", () => { /* 条件 */ const state = { count: 3 }; // (1) const action = { // (2) type: "INCREMENT", payload: { amount: 1 } }; /* テスト対象の処理を実行する */ const actual = reducer(state, action); // (3) /* 処理結果が期待したものになっているかを検証する */ expect(actual).toEqual({ // (4) count: 4 }); }); test("amountが5のときにcountが5増える", () => { ... }); }); describe("type: DECREMENT", () => { test("amountが1のときにcountが1減る", () => { ... }); test("amountが5のときにcountが5減る", () => { ... }); }); describe("type: RESET", () => { test("countが0になる", () => { ... }); });
まずはテストの条件として、更新前の状態(1)と発行されたAction(2)を用意します。これを(3)で実行し、(4)でtoEqual
を用いて検証しています。Reducerが管理する状態はオブジェクトの形になっていることがほとんどなので、値の評価にはtoEqual
を使うことが多くなります。管理上のテクニックとしては、Actionのtypeごとにdescribeでまとめて、payloadの違いによってテストケースを分けるスタイルを採用してみました。
どんな複雑なReducerであっても、基本的な方針は同様です。stateとactionを用意して、更新後のstateが期待したものになっているかを検証します。
まとめ
テスティングフレームワークJestの紹介と、Reducerのテスト作成方法について学んできました。テストコードは作者がどんな意図でロジックを作成し、どんな動作確認を行ったかをコードの形で残せるので、チーム開発においてとても重要なものです。積極的にテストコードを作成して、アプリの品質を高めていきましょう。
次回はもう一歩踏み込んで、Action Creatorや非同期処理のテストについて解説します。