在React项目里引入Redux之后,业务逻辑会被拆解成Reducer、Action Creators、中间件等若干部分。这种拆分带来的最大好处就是可测试性:大部分逻辑不再依赖DOM和组件生命周期,而是变成一个个独立的纯函数。Reducer接收旧的state和action,返回新的state;Action Creator接收参数,返回一个描述动作的普通对象。这两类代码不需要任何渲染环境,用Jest就能直接跑起来。这篇文章详细聊聊如何针对它们编写测试,以及在实践中容易踩到的坑。

为什么Reducer是最好测的代码
Reducer的本质是一个纯函数:(previousState, action) => newState。只要输入确定,输出就确定,不依赖外部变量、不发起网络请求、不修改传入参数。这种特性意味着我们不需要mock任何东西,只需要构造state和action,然后断言返回结果即可。
先看一个典型的待测试Reducer,一个简单的待办事项模块:
// reducers/todos.js
const initialState = [];
export default function todos(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
);
default:
return state;
}
}针对这个Reducer,最基本的测试用例应该覆盖三种情况:未知action时返回默认状态、传入初始状态为undefined时返回initialState、每个分支逻辑产生正确的新状态。用Jest写出来大致如下:
// reducers/todos.test.js
import todos from './todos';
describe('todos reducer', () => {
it('返回初始状态', () => {
expect(todos(undefined, {})).toEqual([]);
});
it('未知action类型时原样返回state', () => {
const state = [{ id: 1, text: '学习', completed: false }];
expect(todos(state, { type: 'UNKNOWN' })).toBe(state);
});
it('ADD_TODO 应追加新待办', () => {
const action = { type: 'ADD_TODO', id: 1, text: '写测试' };
expect(todos([], action)).toEqual([
{ id: 1, text: '写测试', completed: false }
]);
});
it('TOGGLE_TODO 应翻转完成状态', () => {
const state = [{ id: 1, text: '写测试', completed: false }];
const action = { type: 'TOGGLE_TODO', id: 1 };
expect(todos(state, action)).toEqual([
{ id: 1, text: '写测试', completed: true }
]);
});
});这里有两个断言细节值得注意。toEqual做的是递归的值比较,适合比较对象和数组内容;而第二个用例故意用了toBe,它比较的是引用,用来验证default分支确实返回了同一个对象,没有做多余的拷贝。这种区分在实际测试中很有用,能帮你发现不必要的性能开销。
不可变性测试与深层嵌套对象的断言
Redux要求Reducer不直接修改旧state,而是返回新对象。如果团队里有人不小心写了state.push(item)这种代码,功能上看似正常,但会破坏React的重新渲染机制。为了在测试阶段就拦住这类问题,可以引入redux-immutable-state-invariant中间件,或者更直接的方式:在测试中冻结state后再调用Reducer。
使用ES6的Object.freeze即可实现。冻结后的对象一旦被修改就会抛错(严格模式下),任何对旧state的偷袭修改都会让测试直接失败:
import todos from './todos';
it('ADD_TODO 不得修改原state', () => {
const state = Object.freeze([]);
const action = { type: 'ADD_TODO', id: 1, text: '不可变测试' };
expect(() => todos(state, action)).not.toThrow();
});另一种常见做法是在package.json中给Jest加上--snapshot相关配置,配合deep-freeze工具函数递归冻结整个状态树。无论哪种方式,核心思想都是把不可变性从口头约定变成可执行的断言。
当state结构变得深层嵌套时,直接用toEqual比较整个对象会很脆弱,任何一个无关字段的变化都会导致用例失败。更好的策略是只断言你关心的那部分。比如测试一个按ID存储实体的Reducer时,可以这样写:
it('FETCH_SUCCESS 应更新对应实体', () => {
const state = {
byId: { 1: { id: 1, name: '旧名称' } },
allIds: [1]
};
const action = {
type: 'FETCH_SUCCESS',
payload: { id: 1, name: '新名称' }
};
const next = entities(state, action);
// 只断言关键路径,避免无关字段导致误报
expect(next.byId[1].name).toBe('新名称');
expect(next.allIds).toBe(state.allIds);
});这种精确断言配合toBe的引用比较,既能验证数据正确,又能验证未被影响的部分确实没有被重新创建,是大型状态树下非常实用的技巧。
Action Creators的测试策略
Action Creator测试经常被轻视,理由是它太简单了。但恰恰因为简单,测试成本几乎为零,而回报并不低:它能锁住action的shape,防止有人在不通知的情况下改掉payload结构,导致下游Reducer悄悄失效。基础写法就是断言函数返回的对象:
// actions/todos.js
let nextTodoId = 0;
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
});
// actions/todos.test.js
import { addTodo } from './todos';
it('addTodo 返回正确的action对象', () => {
expect(addTodo('写测试')).toEqual({
type: 'ADD_TODO',
id: 0,
text: '写测试'
});
});如果项目使用了redux-thunk,Action Creator可能返回函数而不是对象,这时需要mockdispatch和getState来验证异步流程。一个常用技巧是自己写一个轻量的thunk测试辅助函数:
const thunkTester = ({ dispatch, getState }) => thunk => {
const calls = [];
const mockDispatch = jest.fn(arg => {
if (typeof arg === 'function') {
calls.push(arg(mockDispatch, getState));
} else {
calls.push(arg);
}
return arg;
});
const result = thunk(mockDispatch, getState);
return { dispatch: mockDispatch, calls, result };
};对于action数量庞大的模块,逐个手写断言很枯燥,快照测试是不错的补充。第一次运行时Jest会生成快照文件,之后每次测试都会与快照对比,结构一变立即报错,你只需确认变更是有意为之并执行更新。需要注意的是,快照测试只验证形状,无法验证业务含义,所以关键action还是建议保留显式的toEqual断言,快照作为兜底。
最后提一下组合层面的测试。单元测试过关不代表整个应用状态流转正确,建议在所有子Reducer测试通过后,再用combineReducers组装的根Reducer跑几个端到端式的用例:连续dispatch一组action,断言最终状态树符合预期。这种集成式测试数量不用多,挑三五个核心业务流程即可,能有效捕捉子模块之间的衔接问题。测试文件的组织上,把测试文件和源码放在同一目录(如todos.js旁边放todos.test.js),查找和迁移都更方便。坚持这套流程,Redux部分的测试覆盖率会很容易维持在高水平,重构时也就有了底气。
Redux测试Reducer测试Action Creators修改时间:2026-09-04 17:08:54