测试action创建器
测试同步action创建器
同步action创建器可选地接收参数并返回一个普通对象,称为action。它们是产生确定性结果的纯函数。请务必导出action创建器,以使其在测试文件中可用
action.js
export function createTasksSucceeded(task){
return {
type: CREATE_TASK_SUCCEEDED,
payload: {
task
}
}
}
测试同步action创建器代码
import {createTasksSucceeded} from './actions/';
describe('action creators', ()=>{
it('should handle successful task creation', ()=>{
const task = {
title: 'Get schwifty',
description: 'Show me what you got'
}
const expectdAction = {
type: CREATE_TASK_SUCCEEDED,
payload: { task }
}
expect(createTaskSucceeded(task)).toEqual(expectedAction);
})
})
测试异步action创建器
createTask异步创建器
export function createTask({title, description, status = 'Unstarted'}) {
return dispatch => {
dispatch(createTaskRequested());
return api.createTask({title, description, status}).then(resp =>{
dispatch(createTaskSucceeded(resp.data))
})
}
}
需要注意的是,将在action创建器内返回由api.createTask返回的promise。让异步action创建器返回promise通常是一种很好的开发实践,因为这可让调用者灵活响应promise的结果。
我们需要一个额外的软件包 redux-mock-store。它为我们提供了一个便捷的接口 - store.getActions(),该接口返回已经派发至模拟store的action列表。可据此断言createTask正确派发了用于表示请求开始/成功的action。另外唯一需要的是Jest,可用它手动模拟API响应。从配置模拟store开始,最终将使用此配置派发createTask。还可导入并应用createTask依赖的redux-thunk中间件
配置模拟Redux store
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {createTask} from './';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
接下来,模拟api.createTask.可使用Jest模拟工具模拟api.createTask。可确保api.createTask返回在测试中直接控制的promise,并且不需担心任何与HTTP相关的问题
jest.unmock('../api');
import * as api from '../api';
api.createTask = jest.fn(
() => new Promise((resolve, reject) => resolve({data: 'foo'}))
)
至此,我们完成了大部父配置,是时候开始测试了。
describe('createTask', ()=>{
it('works', ()=>{
const expectedActions = [
{type: CREATE_TASK_STARTED},
{type: CREATE_TASK_SUCCEEDED, payload: {task: 'foo'}}
]
const store = mockStore({
tasks: {
tasks: []
}
})
return store.dispatch(createTask({})).then(()=>{
expect(store.getActions()).toEqual(expectedActions);
expect(api.createTask).toHaveBeenCalled();
})
})
})
测试saga
saga最适合用于处理更复杂的副作用,比如长时间运行的进程。
测试一个计时器saga
import {delay} from 'redux-daga';
import {call, put} from 'redux-saga/effects';
export function* handleProgressTimer({type, payload}){
if(type === TIMER_STATED){
while(true){
yield call(delay, 1000);
yield put({
type: TIMER_INCREMENT,
payload: {taskId: payload.taskId}
})
}
}
}
saga中间件可执行ajax请求或其他副作用。你所编写的saga会返回一个effect:一个描述中间件如何处理逻辑的对象。在测试saga时,需要断言的是生成器函数是否返回期望的effect对象。
生成器的每个返回值都是具有value和done两个键的对象。在测试TIMER_STARTED时,每次生成器调用next函数时,都将断言value键是否符合期望。但可通过调用saga方法来生成预期输出,而非手动输入effect对象。
import {delay} from 'redux-saga';
import {call, put} from 'redux-saga/effects';
import {handleProgressTimer} from '../sagas';
describe('sagas', ()=>{
it('handles the handleProgressTimer happy path', ()=>{
const iterator = handleProgressTimer({
type: TIMER_STARTED,
payload: {taskId: 12}
})
const expectedAction = {
type: TIMER_INCREMENT,
payload: {taskId: 12}
}
expect(iterator.next().value).toEqual(call(delay, 1000));
expect(iterator.next().value).toEqual(put(expectedAction));
expect(iterator.next().value).toEqual(call(delay, 1000));
expect(iterator.next().value).toEqual(put(expectedAction));
expect(iterator.next().done).toBe(false);
})
it('handles the handleProgressTimer sad path', ()=>{
const iterator = handleProgressTimer({
type: TIMER_STOPPED,
})
expect(iterator.next().done).toBe(true)
})
})
测试saga很简单,最重要的想法是逐步遍历生成器函数的结果。假设saga有结论,那么最终可断言done的值为true.
测试reducer
tasks reducer
const initialState = {
tasks: [],
isLoading: false,
error: null,
seachTerm: '',
}
export default function tasks(state = initialState, action){
switch(action.type){
case FETCH_TASKS_STARTED: {
return {
...state,
isLoading: true,
}
}
case FETCH_TASKS_SUCCEEDED: {
return {
...state,
tasks: action.payload.tasks,
isLoading: false
}
}
...
default: {
return state;
}
}
}
因为reducer是纯函数,所以测试时不需要任何特殊的辅助函数与其他业务逻辑。 测试tasks reducer
import tasks from '../reducers/';
describe('the tasks reducer', ()=>{
const initialState = {
tasks: [],
isLoading: false,
error: null,
searchTerm: '',
}
it('should return the initialState', ()=>{
expect(tasks(undefined, {})).toEqual(initialState);
})
it('should handle the FETCH_TASKS_STARTED action', ()=>{
const action = {type: FETCH_TASKS_STARTED};
const expectedState = {
...initialState,
lsLoading: true
}
expect(tasks(initialState, action)).toEqual(expectedState);
})
it('should handle the FETCH_TASKS_SUCCEEDED action', ()=>{
const tasksList = [{title: 'Test the reducer', description: 'Very meta'}];
const action = {
type: FETCH_TASKS_SCCEEDED,
payload: {tasks: tasksList}
}
const expectedState = {
...initialState,
tasks: taskList
}
expect(tasks(initialState, action)).toEqual(expectedState);
})
})
在断言之前声明action和期望状态的变量。这种模式是为了提高断言的可读性。
测试选择器
选择器可以是纯函数,但是使用reselect创建的选择器具有附加功能 - memoization。 下面代码引入了两个普通选择器和一个reselect创建的选择器。
示例选择器
import {createSelector} from 'reselect';
export const getTasks = state => state.tasks.tasks;
export const getSearchTerm = state => state.tasks.searchTerm;
export const getFilteredTasks = createSelector(
[getTasks, getSearchTerm],
(tasks, searchTerm) => {
return tasks.filter(tasks => task.title.match(new RegExp(searchTerm, 'i')))
}
)
使用reselect编写的选择器更有趣一些,因此reselect提供了额外的辅助函数。除了返回期望的输出外,另一个值得测试的关键功能是,函数在缓存正确的数据并限制其执行的重新计算的次数。辅助函数recomputations专用于此。
普通选择器接收状态作为输入,并产生状态切片作为输出。reselect选择器执行类似的任务,但会尽其所能避免多余的计算工作。如果getFilteredTasks函数具有相同的输入,则无须重复计算输出,而是选择返回最近存储的输出。
在不同测试之间,可用resetRecomputations函数将数字重置为0
import {getTasks, getSearchTerm, getFilteredTasks} from '../reducers/';
import cloneDeep from 'lodash/cloneDeep';
describe('tasks selectors', ()=>{
const state = {
tasks: {
tasks: [
{title: 'Test selectors', description: 'Very meta'},
{title: 'Learn Redux', description: 'Oh my'},
],
searchTerm: 'red',
isLoading: false,
error: null,
}
};
afterEach(()=>{
getFilteredTasks.resetRecomputations();
})
it('should retrieve tasks from the getTasks selector', ()=>{
expect(getTasks(state)).toEqual(state.tasks.tasks);
})
it('should retrieve the searchTerm from the getSearchTerm selector', ()=>{
expect(getSearchTerm(state)).toEqual(state.tasks.searchTerm);
})
it('should minimally recompute the state when getFilteredTasks is called', ()=>{
const similarSearch = cloneDeep(state);
similarSearch.tasks.searchTerm = 'redu';
const uniqueSearch = cloneDeep(state);
uniqueSearch.tasks.searchTerm = 'selec';
// 验证选择器执行了最少次数的重新计算
expect(getFilteredTasks.recomputations()).toEqual(0);
getFilteredTasks(state);
getFilteredTasks(similarDearch);
expect(getFilteredTasks.recomputations()).toEqual(1);
getFilteredTasks(uniqueSearch);
expect(getFilteredTasks.recomputations()).toEqual(2);
})
})
测试容器组件
容器组件是可访问redux store的组件。当使用来自react-redux的connect方法包装组件并导出时,表明组件是容器组件。
测试容器组件时需考虑普通组件之外的一些额外问题。例如,除了dispatch方法和mapStateToProps函数中指定的任何其他属性,组件将获取额外的可用属性。另一个问题是,实际导出的组件并非编写的组件,而是增强后的组件。
测试容器组件时,为避免未发现实例错误,需在类声明中添加export关键字
export class App extends Component
describe('the app container', () => {
const spy = jest.fn();
const wrapper = mount(<App dispatch={sqy} erro ="Boom!" tasks={[]} .>);
expect(spy).toHaveBeenCalled();
})
以这种方式测试容器组件通常足以满足期望的测试覆盖率。但是,你可能会对测试Redux功能感兴趣,此时需要使用一个包含store实例的Provider组件包装容器组件。
访问store实例是使用工具软件包的一种常见需求。可使用redux-mock-store
npm i -D redux-mock-store
import {Provider} from 'react-redux';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import ConnectedApp, {App} from '../App';
describe('the App container', ()=>{
it('should fetch tasks on mount', ()=>{
const middlewares = [thunk];
const initialState = {
tasks: {
tasks: [],
isLoading: false,
error: null,
searchTerm: '',
}
};
const mockStore = configureMockStore(middlewares)(initialState);
const wrapper = mount(<Provider store={mockStore}><ConnectedApp/></Provider>);
const expectedAction = {type: FETCH_TASKS_STARTED};
expect(mockStore.getActions()[0]).toEqual(expectedAction);
})
})