如何用Jest和React Testing Library Mock React组件多个useState
非常不推荐直接Mock组件内部的useState状态。React Testing Library从设计之初就鼓励大家站在用户视角写测试:用户根本不会关心你组件里定义了几个state、叫什么名字,只会点按钮、看页面反馈的内容。要是直接mock内部的useState,相当于测试和代码实现强绑定——哪天你重构把isClick改名叫isSubmitted,或者换成useReducer管理状态,明明功能一点没坏,测试先报错了,这种测试除了给重构添堵没什么意义。
你完全不需要关心setIsClick有没有被调用、state有没有变成true,只要模拟用户的真实操作,然后断言操作后页面上的可见变化符合预期就行。
举个例子,如果isClick === true时页面会渲染「提交成功」的提示文案,测试代码可以这么写:
import { render, screen, fireEvent } from '@testing-library/react'; import Component from './Component'; test('点击提交按钮后显示提交成功提示', () => { render(<Component />); // 初始状态下提示不存在 expect(screen.queryByText('提交成功')).not.toBeInTheDocument(); // 模拟用户点击提交按钮 fireEvent.click(screen.getByRole('button', { name: /submit/i })); // 断言用户能看到提交成功的提示 expect(screen.getByText('提交成功')).toBeInTheDocument(); });
顺便提一句:你贴的示例代码有两个明显问题:一是button元素不支持
onSubmit事件,应该换成onClick,onSubmit是form标签的事件;二是button的闭合标签写错了,</>应该改成</button>。
如果真遇到极端场景——比如状态暂时没有对应UI渲染、或者需要隔离测试某段逻辑,必须mock useState的话,可以通过jest监听React模块的方式实现,注意要保留React其他方法的原有逻辑,只覆写需要修改的useState逻辑:
import { render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; import Component from './Component'; // 先存一份原始的useState实现,避免全局污染 const originalUseState = React.useState; // 监听React.useState,自定义实现 jest.spyOn(React, 'useState').mockImplementation((initialValue) => { // 注意:useState是按调用顺序返回状态的,这里的判断要和组件内useState的定义顺序完全对应 // 你组件里第二个useState是isClick,初始值为false,这里匹配到就返回mock值 const callCount = React.useState.mock.calls.length; if (callCount === 2 && initialValue === false) { // 返回mock的状态值,以及一个mock的setter函数 return [true, jest.fn()]; } // 其他状态走原始逻辑 return originalUseState(initialValue); }); test('isClick为true时的逻辑测试', () => { render(<Component />); // 此时组件内isClick的初始值就是我们mock的true,直接写对应断言即可 }); // 测试结束后一定要恢复原始实现,不然会影响其他测试用例 afterAll(() => { jest.restoreAllMocks(); });
要提醒的是,这种mock方式非常脆弱:只要你调整组件里useState的定义顺序、改了初始值,mock就会直接失效,除非万不得已不要用。
内容的提问来源于stack exchange,提问作者Akshay phalphale

