导读:本期聚焦于黑豹创作的《React中Redux测试怎么做?Reducer纯函数与Action Creators的测试方法详解》,敬请观看详情。单元测试该覆盖哪些内容?Reducer作为纯函数,输入相同必然得到相同输出,这种特性让它成为最容易测试的代码之一。本文围绕Reducer测试展开,讲解如何针对初始状态、状态变更和不可变性编写用例,结合Jest与expect扩展处理深层嵌套对象的断言。同时介绍Action Creators的测试思路,说明为什么简单函数测试也要注意返回对象的结构完整性,以及如何借助快照测试降低维护成本。文中还给出combineReducers场景下的测试建议,帮助你在React项目中搭建稳定的Redux测试体系。

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

React中Redux测试怎么做?Reducer纯函数与Action Creators的测试方法详解

为什么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可能返回函数而不是对象,这时需要mockdispatchgetState来验证异步流程。一个常用技巧是自己写一个轻量的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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260904/50366.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。