React + Redux アプリケーションのテストを考えているときに、reduxのテストの書き方のドキュメントと、丁度以下の記事を目にしたので、自身のプロジェクトに当てはめてみる
上の記事で挙げられているテストの対象は以下
Action Creator
自身のプロジェクトではAction Creator
はActionTypeと引数をそのまま渡す程度の薄い関数のため書くべきか悩むが、メリットはあるため優先度を下げて対応を行うことにする
書くとしたらこんな感じ
export function addTodo(text) { return { type: 'ADD_TODO', text } }
import * as actions from '../../actions/TodoActions' import * as types from '../../constants/ActionTypes' describe('actions', () => { it('should create an action to add a todo', () => { const text = 'Finish docs' const expectedAction = { type: types.ADD_TODO, text } expect(actions.addTodo(text)).toEqual(expectedAction) }) })
Reducer
Reducer内にロジックを持っているため厚くテストを書いておきたい
以下に対するテストを忘れすに行う
- ロジック
- 正常系
- 異常系
- 境界値
import { ADD_TODO } from '../constants/ActionTypes' const initialState = [ { text: 'Use Redux', completed: false, id: 0 } ] export default function todos(state = initialState, action) { switch (action.type) { case ADD_TODO: return [ { id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, completed: false, text: action.text }, ...state ] default: return state } }
import reducer from '../../structuring-reducers/todos' import * as types from '../../constants/ActionTypes' describe('todos reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual([ { text: 'Use Redux', completed: false, id: 0 } ]) }) it('should handle ADD_TODO', () => { expect( reducer([], { type: types.ADD_TODO, text: 'Run the tests' }) ).toEqual([ { text: 'Run the tests', completed: false, id: 0 } ]) expect( reducer( [ { text: 'Use Redux', completed: false, id: 0 } ], { type: types.ADD_TODO, text: 'Run the tests' } ) ).toEqual([ { text: 'Run the tests', completed: false, id: 1 }, { text: 'Use Redux', completed: false, id: 0 } ]) }) })
Selector
Selectorを積極的に使っていなかったので、これを機に使うようにする
Middleware
権限周りを扱うかなり重要なMiddlewareがあるため、その辺りを重点的にテストする
const thunk = ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState) } return next(action) }
const create = () => { const store = { getState: jest.fn(() => ({})), dispatch: jest.fn() } const next = jest.fn() const invoke = action => thunk(store)(next)(action) return { store, next, invoke } } it('passes through non-function action', () => { const { next, invoke } = create() const action = { type: 'TEST' } invoke(action) expect(next).toHaveBeenCalledWith(action) }) it('calls the function', () => { const { next, invoke } = create() const fn = jest.fn() invoke(fn) expect(next).toHaveBeenCalled() }) it('passes dispatch and getState', () => { const { store, invoke } = create() invoke((dispatch, getState) => { dispatch('TEST DISPATCH') getState() }) expect(store.dispatch).toHaveBeenCalledWith('TEST DISPATCH') expect(store.getState).toHaveBeenCalled() })
API通信処理
Fetch処理は外部ライブラリで行っているため書かない