2026年5月31日 11:08

Jest 如何测试 Redux 的 Action、Reducer 和 Selector?

Redux 测试最好按代码职责拆开:action creator 测返回的 action,reducer 测状态如何变化,selector 测派生数据,异步 thunk 或 RTK Query 再测副作用边界。不要把所有东西都塞进一个 React 组件测试里,否则失败时很难判断是 UI、store、接口 Mock,还是 reducer 写错了。Jest 的价值在于让这些纯逻辑可以被快速、稳定地验证。

从 reducer 开始最划算

reducer 通常是纯函数,输入旧 state 和 action,输出新 state。它不需要 DOM,也不需要 mock store,是 Redux 里最适合单元测试的部分。

js
const initialState = { count: 0 } function counterReducer(state = initialState, action) { switch (action.type) { case 'counter/increment': return { ...state, count: state.count + 1 } case 'counter/add': return { ...state, count: state.count + action.payload } default: return state } } test('处理 add action', () => { expect(counterReducer({ count: 2 }, { type: 'counter/add', payload: 3 })) .toEqual({ count: 5 }) })

这里要特别测 default 分支和不可变更新。一个常见坑是 reducer 直接修改原对象,简单断言结果可能看不出来,但会影响 React-Redux 的引用比较。可以在测试里冻结输入对象,或者使用 Redux Toolkit 的 Immer,让写法和不可变语义保持一致。

action 和 selector 怎么测?

action creator 如果只是返回对象,测试应保持克制;过度测试会让代码变得像快照翻译。selector 更值得测,因为它经常藏着筛选、排序、权限判断和空值兼容。

js
export const addTodo = text => ({ type: 'todos/add', payload: text }) export const selectDoneTodos = state => state.todos.items.filter(x => x.done) test('创建 addTodo action', () => { expect(addTodo('写测试')).toEqual({ type: 'todos/add', payload: '写测试' }) }) test('筛选已完成 todos', () => { const state = { todos: { items: [{ done: true }, { done: false }] } } expect(selectDoneTodos(state)).toHaveLength(1) })

selector 的边界包括空数组、缺字段、排序稳定性和 memoized selector 的引用复用。尤其是 Reselect,如果 selector 每次都返回新数组,组件会无意义重渲染。

异步逻辑如何隔离?

传统 thunk 可以 mock API,再断言 dispatch 顺序。Redux Toolkit 的 createAsyncThunk 更推荐测 fulfilled/rejected 对 reducer 的影响,少测内部实现。

js
test('fetchUser 成功后派发 success', async () => { const api = { getUser: jest.fn().mockResolvedValue({ id: 1 }) } const dispatch = jest.fn() await fetchUser(1, api)(dispatch) expect(dispatch).toHaveBeenCalledWith({ type: 'user/success', payload: { id: 1 } }) })

追问

为什么 reducer 测试比 action 测试更重要?

reducer 承担业务规则,状态加减、列表合并、错误回滚都在这里发生。action creator 很多时候只是对象工厂,测太细会带来重复断言。取舍是:复杂 action payload 需要测试,简单 action 可以靠 reducer 或集成测试覆盖。踩坑是只测 action 不测 reducer,最后 action 发出去了但状态根本没变。

selector 应该测返回值还是 memoization?

普通 selector 测返回值即可,重点是不同 state 下结果是否符合业务预期。使用 Reselect 时,可以补一两个引用稳定性的测试,确认相同输入不会重新生成对象。边界是不要把 memoization 细节测得过死,否则换实现时测试会阻碍重构。真正有性能风险的 selector,例如大列表过滤和权限树计算,才值得额外测缓存行为。

mock store 和真实 store 怎么选?

mock store 适合验证 thunk 派发了哪些 action,速度快,也容易断言顺序。真实 store 更适合验证 reducer、middleware 和组件一起工作后的最终 UI。取舍点是你关心“过程”还是“结果”:过程用 mock store,结果用真实 store。坑在于 redux-mock-store 不会真的更新 state,拿它测组件状态变化会得到误导性结果。

Redux Toolkit 的 slice 还需要单独测 action 吗?

大多数情况下不需要单独测自动生成的 action creator,因为它们由库保证。更有价值的是测试 slice reducer 对 pending、fulfilled、rejected 的处理,以及 payload 边界。边界是如果你写了 prepare 回调,它包含格式化、生成 id 或清洗输入,就应该单测。否则测试越多越像在测试 Redux Toolkit 本身。

React-Redux 组件测试要 mock useSelector 吗?

可以 mock,但不建议作为默认方案。mock useSelector 很快,却会绕过 Provider、store shape 和订阅更新,容易让测试与真实运行环境脱节。更稳的方式是创建一个测试 store,用 Provider 包住组件,然后从用户视角断言页面。只有组件很小、依赖 state 很简单,并且你明确只想隔离 UI 分支时,mock hook 才是合理取舍。

标签:Jest