You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何测试React Hooks组件:模拟useState与异步submitForm

解决React Hooks组件的Jest/Enzyme测试问题

我来帮你搞定这个React Hooks组件的测试难题——核心是要正确处理useState的模拟和异步submitForm的测试逻辑,下面分步骤给你讲清楚:

1. 测试不同redirect状态下的渲染

你之前尝试直接mock全局的React.useState是踩了坑的,因为组件里调用了两次useState(一次管理formValues,一次管理redirect),全局mock会导致两个状态的setter混在一起。正确的做法是分别mock两次useState的调用,指定不同的初始值:

describe('<Parent /> basic rendering', () => {
  afterEach(() => {
    jest.restoreAllMocks(); // 每次测试后恢复useState,避免影响其他测试
  });

  it('渲染Form组件当redirect为false时', () => {
    // 第一次mock:formValues的初始值和setter
    jest.spyOn(React, 'useState').mockImplementationOnce(() => [{}, jest.fn()]);
    // 第二次mock:redirect的初始值设为false
    jest.spyOn(React, 'useState').mockImplementationOnce(() => [false, jest.fn()]);

    const wrapper = shallow(<Parent {...createTestProps()} />);
    expect(wrapper.find(Form)).toHaveLength(1);
    expect(wrapper.find(Redirect)).toHaveLength(0);
  });

  it('渲染Redirect组件当redirect为true时', () => {
    jest.spyOn(React, 'useState').mockImplementationOnce(() => [{}, jest.fn()]);
    jest.spyOn(React, 'useState').mockImplementationOnce(() => [true, jest.fn()]);

    const wrapper = shallow(<Parent {...createTestProps()} />);
    expect(wrapper.find(Redirect)).toHaveLength(1);
    expect(wrapper.find(Form)).toHaveLength(0);
  });
});

2. 模拟异步submitForm触发状态变化

这是更贴近真实业务场景的测试方式:通过模拟submitForm的异步返回值,触发组件内部的setRedirect,然后验证渲染变化。注意函数组件没有instance()方法,我们需要从Form组件的props里拿到onSubmit方法:

describe('<Parent /> onSubmit行为测试', () => {
  let props;
  beforeEach(() => {
    props = createTestProps();
    jest.clearAllMocks();
  });

  it('当submitForm返回真值时,设置redirect为true并渲染Redirect', async () => {
    // 模拟submitForm异步返回true
    props.submitForm = jest.fn().mockResolvedValue(true);
    const wrapper = shallow(<Parent {...props} />);

    // 调用组件的onSubmit方法
    await wrapper.find(Form).prop('onSubmit')();
    // 强制组件更新,因为异步操作后状态变化需要刷新渲染
    wrapper.update();

    expect(props.submitForm).toHaveBeenCalledWith({});
    expect(wrapper.find(Redirect)).toHaveLength(1);
    expect(wrapper.find(Form)).toHaveLength(0);
  });

  it('当submitForm返回假值时,保持redirect为false并渲染Form', async () => {
    props.submitForm = jest.fn().mockResolvedValue(false);
    const wrapper = shallow(<Parent {...props} />);

    await wrapper.find(Form).prop('onSubmit')();
    wrapper.update();

    expect(wrapper.find(Form)).toHaveLength(1);
    expect(wrapper.find(Redirect)).toHaveLength(0);
  });

  it('当submitForm抛出错误时,打印错误并保持redirect为false', async () => {
    const mockError = new Error('提交失败');
    props.submitForm = jest.fn().mockRejectedValue(mockError);
    // 模拟console.log,避免测试时输出真实日志
    const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});

    const wrapper = shallow(<Parent {...props} />);
    await wrapper.find(Form).prop('onSubmit')();
    wrapper.update();

    expect(consoleSpy).toHaveBeenCalledWith('Submit error: ', mockError);
    expect(wrapper.find(Form)).toHaveLength(1);

    consoleSpy.mockRestore(); // 恢复console.log
  });
});

3. 修正你现有测试的问题

你之前的测试有几个关键错误:

  • 全局mockReact.useState会导致测试之间相互污染,每次测试后一定要restore
  • 试图直接赋值setRedirect是无效的,需要通过mockImplementationOnce区分两次useState调用
  • 测试逻辑写反了:redirect为false时应该渲染Form,为true时才渲染Redirect

总结

优先选择模拟真实交互+异步函数返回值的测试方式,这样更符合用户实际使用流程;如果需要单独测试不同状态的渲染,再针对性mockuseState的调用。

内容的提问来源于stack exchange,提问作者Adrian Bartholomew

火山引擎 最新活动