やること
Jestを使って、ReduxのAction、特にHTTPリクエストなどの非同期処理が絡む部分をテストしたい。
特に今回の場合、すべてのActionがStoreからdispatchされていることを確認したいとする。
自分の場合、HTTPリクエストのライブラリはaxios
を使用している。そのため今回のテストでは、axios
を使ったリクエストをモックできるaxios-mock-adapter
というライブラリを使用する。
モックというのは直訳で「マネする」の意。つまり、axios
のリクエストをモックする、という言い方は「axiosのリクエストを擬似的に送って、擬似的なレスポンスを受け取る処理を行う」というような意味合いになる。
そのため、axios-mock-adapter
のようなHTTPリクエストのモックライブラリを使うことで、実際のリクエストを送ることなく以下の点などを確かめることができる。
- 対象のエンドポイントに正しくリクエストが送れるか。
- 帰ってきたレスポンスをもとに後続処理が継続できるか
また、今回は全てのアクションが正しくdispatchされていることを確認するのが目的のため、redux-mock-store
というライブラリを使用する。これを使うと、ReduxのStoreをモックして、dispatchされたアクションを確認することができる。Jestと併用することで実際にdispatchされたアクションと期待するアクションを比較したりできる。
前提
以下のような非同期のアクションがあるとする(解説のため処理を簡略化)。
import axios from 'axios';
export const auth = authData => {
return dispatch => {
const url = `http:/localhost:3001/api/auth/sign_in`;
// Promiseを返すようにするのを忘れずに
return axios
.post(url, authData)
.then(response => {
dispatch(authSuccess(response.data));
})
.catch(error => {
dispatch(authFail(error));
});
};
};
メールアドレスとパスワードを渡してエンドポイントにPOSTし、認証情報があっていれば認証成功となりStoreにユーザー情報を格納する。間違っていればエラー情報をStoreに格納する。
このアクションをテストしてみる。
手順
axios-mock-adapter
まずaxios-mock-adapter
をプロジェクトに導入する。
公式ドキュメントを見ればだいたい分かる。
https://www.npmjs.com/package/axios-mock-adapter
$ npm install --save-dev axios-mock-adapter
auth.test.js
ファイルを作成し、axios-mock-adapterをインポート。
セットアップにaxios
が必要になるので一緒にインポートする。
import axios from 'axios';
import axiosMockAdapter from 'axios-mock-adapter';
adapterのモックを作成する。
const axiosMock = new axiosMockAdapter(axios);
引数にaxios
を渡す必要があるため先述の通りインポートした。
今回はPOSTリクエストを投げるので、以下の形式でモックのリクエストを記述する。
//HTTPリクエストを送るエンドポイント
const url = `http://localhost:3001/api/auth/sign_in`;
//POST時に渡すデータ
const authData = {
email: 'test@example.com',
password: 'password'
}
//帰ってくるレスポンス
const mockResponse = {
token: 'abcdefg',
uid: 1,
}
//モックのPOSTリクエストを投げる
axiosMock.onPost(
url, //リクエスト先URL
authData //POSTするデータ
).reply(
200, //ステータス
mockResponse //レスポンス
// [第三引数にHeadersの情報も載せられる]
);
他のメソッドやオプションについては公式ドキュメントを参照。
https://www.npmjs.com/package/axios-mock-adapter
redux-mock-store
同じくredux-mock-storeを導入する。
公式ドキュメント。
https://www.npmjs.com/package/redux-mock-store
$ npm install --save-dev redux-mock-store
インポートする。Storeを作る際にmiddlewareを渡すのでredux-thunk
も併せてインポート。
import thunk from 'redux-thunk'; //セットアップに必要
import configureStore from 'redux-mock-store';
Storeを作成する。必要であればmockStore({})
にはinitial stateを設定することも可能。
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const store = mockStore({});
これでモックのStoreの情報を以下のように操作できる。
store.dispatch(action) //モックのstoreからアクションをdispatchする
store.getActions() //dispatchされたアクションが配列で返ってくる
store.getState() //Storeのstateがオブジェクトで返ってくる
これも公式ドキュメントで詳細を参照。
https://www.npmjs.com/package/redux-mock-store
テストファイル
最終的にテストファイルは以下のようになる。
import * as actionTypes from './actionTypes';
import * as actions from './auth';
import axios from 'axios';
import axiosMockAdapter from 'axios-mock-adapter';
describe('async actions', () => {
const axiosMock = new axiosMockAdapter(axios);
it('creates AUTH_SUCCESS if authentication succeeds', () => {
// redux-mock-storeのセットアップ。ここでモックのStoreを作成する
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const store = mockStore({});
//最終的にdispatchを期待するアクション。今回は1つだが複数を想定するため配列に格納
const expectedActions = [
{
type: actionTypes.AUTH_SUCCESS,
token: 'abcdefg',
uid: 1,
}
]
// HTTPリクエストを送るエンドポイント
const url = `http://localhost:3001/api/auth/sign_in`;
//POST時に渡すデータ
const authData = {
email: 'test@example.com',
password: 'password'
}
//帰ってくるレスポンス
const mockResponse = {
token: 'abcdefg',
uid: 1,
}
//モックのPOSTリクエストを投げる
axiosMock.onPost(
url,
authData
).reply(
200,
mockResponse
);
// テストするアクションをdispatch
return store.dispatch(actions.auth(authData))
.then(() => {
// 最終的にモックでdispatchされたActionと期待するActionを比較する
expect(store.getActions()).toEqual(expectedActions);
})
});
});
これでdispatchされたアクションと期待するアクションが一致して、テストが通るはず。
つまづいたところ
axiosMock.onPost()
で指定する第二、第三引数のresponse
とheaders
は、以下の形式で返ってくる。
response: {},
headers: {}
これに気づかずに、以下のようなオブジェクトを引数に渡していて、dispatchされたアクションのpayloadがundefinedになってしまった。
const mockResponse = {
response: {
token: 'abcdefg',
uid: 1
}
}
const mockHeaders = {
headers: {
'access-token': 'abcdefg',
uid: 1
}
}
つまり、上記のようにそれぞれresponse
, headers
というキーは不要。