通信を伴うAction Creatorのテスト
非同期処理のテスト方法についてはここまで解説してきた通りですが、通信を伴うAction Creatorをテストするにあたっては、もう少し工夫が必要になります。これまで学んできたテストケースの作り方では対処ができない、次の2種類のテストを実施できるようにしなければいけません。
- 外部環境とのデータ授受で得られたデータを条件に持つテスト
- 引数として渡された値を検査するテスト
それぞれの具体的な内容を解説します。
外部環境とのデータ授受で得られたデータを条件に持つテスト
テストケースを作成するにあたって、常に関数を実行する条件は同じであることが望ましいです。テストケースの内部に記述したデータは、テストを実行するごとにリセットされるため、望ましい状態であるといえるでしょう。しかし、関数が内部でサーバーやデータベースにアクセスしている場合、前回テストを実行したときの結果が残ってしまったり、同じテストを実行している同僚の実行結果が残っていたりして、同じデータを使えない状況が発生します。
非同期のAction Creatorは、内部でサーバーのAPIなどを呼び出していることがあります。一般的に、自動テストを作成する際には、テストケースの中ですべての条件を記述できるのが好ましいです。しかし、サーバーにアクセスしてしまうと、サーバー側の状態に応じてテスト結果が変わってしまう恐れがあります。これは、あまりうれしい状況ではありません。
こういった場合に取る対策としては、大別すると次の2つがあります。
- テストケースごとに通信処理を偽装して、固定の値しか返ってこないようにする
- テストケースごとにテスト用のAPIサーバーを起動して、固定の値しか返ってこないようにする
後者は大掛かりになってしまうので、本記事では前者の方針を採ることにします。偽装とはいっても、特殊なツールを使うわけではありません。通信用のモジュールに定義されている関数を、テストケースの中で上書きするだけです(リスト9)。
import ApiClient from "../api/ApiClient"; // fetchPosts関数が固定の結果しか返さないようにする ApiClient.fetchPosts = () => Promise.resolve([ { id: 1, name: "なかがわ", age: "thirties", body: "なかがわです" }, { id: 2, name: "たなか", age: "fourties", body: "たなかです" }, { id: 3, name: "すずき", age: "teen", body: "すずきです" }, ]);
テストケースの序盤にリスト9の処理を実行しておけば、その後にAction Creatorを実行した際にも内部では偽装した処理のほうが実行されます。これにより、常に意図した条件下でテストが実施されるようになります。
引数として渡された値を検査するテスト
テストケースの主な目的は、処理の結果としてどんな値が生まれたのかを検査することです。これまで学んできた範囲では、関数の戻り値やPromiseの結果を検査していました。しかし、リスト10のような、非同期なAction Creatorにありがちな通信処理の場合はどうでしょうか。
function doGet(a, b, dispatch) { fetch(`https://example.com?a=${a}&b=${b}`) .then(response => { dispatch({ type: "HOGE", payload: response }); // (1) }); }
ここで処理結果として検査したいのは、(1)でdispatch
関数の引数として渡されたオブジェクトです。Reduxの性質上、dispatch
関数を実行することでしかアプリケーションの状態を更新することができないので、dispatch
関数に渡すActionオブジェクトをAction Creatorの成果、つまり検査対象として扱っても問題ないだろう、といった考え方です。
さて、dispatch
関数の引数に何が渡されたのかをチェックしたいわけですが、幸い、Jestにはこういった用途に最適な機能が用意されています。**モック関数**と呼ばれる機能です。次節で解説します。
Jestのモック関数機能について
前述の通り、Jestには関数呼び出しを記録・検査するための仕組みとして「モック関数(Mock Functions)」と呼ばれる機能が用意されています。詳しいドキュメントはこちらのリンクをご覧ください。
本記事では、引数の調べ方のみを解説します。リスト11が実行例です。
function hoge(callback) { callback(1); // (a) callback(2, 3); // (b) callback(4); // (c) } test("sample", () => { const mockFn = jest.fn(); // (1) hoge(mockFn); // (2) const calls = mockFn.mock.calls; // (3) expect(calls[0]).toEqual([1]); // (a)で渡した引数 expect(calls[1]).toEqual([2, 3]); // (b)で渡した引数 expect(calls[2]).toEqual([4]); // (c)で渡した引数 });
まず、グローバル変数jest
から、jest.fn()
を呼び出すことでモック関数オブジェクトを作り出します(1)。これをdispatch
等の代わりに引数としてテスト対象の関数に渡す(2)ことで、内部でどのような呼び出され方をしているのかを記録できます。テスト対象の関数が実行された後、モック関数オブジェクトの.mock.calls
プロパティ(3)には、内部でどんな引数が渡されたのかが2次元配列で記録されています。このcalls
内のデータを検査することで、内部での処理結果をうかがい知ることができます。