如何解决Jest无法监视/模拟React组件箭头函数及handleClick的问题?
嘿,这个问题我之前踩过不少坑,来给你捋清楚怎么解决!先直接给你答案:当然可以用Jest监视/模拟React组件里的胖箭头类实例方法,但这里有个关键坑得注意——胖箭头方法是直接绑定在类实例上的,而不是原型链上,常规的spyOn原型的方式根本行不通。
一、为啥胖箭头方法常规Spy会失效?
咱们写的胖箭头类方法,比如handleClick = () => {},是在类实例化的时候直接挂载到实例本身上的,不像普通类方法那样存在于类的原型链里。所以如果你用jest.spyOn(MyComponent.prototype, 'handleClick')这种针对原型的操作,对胖箭头方法完全没用——因为实例上的方法早就把原型的覆盖了(甚至原型上根本没这个方法)。
二、正确监视/模拟胖箭头类实例方法的两种路子
路子1:实例化组件前先替换类的方法
如果你的组件是这么写的:
class MyComponent extends React.Component { handleClick = () => { // 你的业务逻辑 } render() { return <button onClick={this.handleClick}>点我呀</button> } }
那你可以在测试代码里,提前把类的handleClick换成mock函数:
// 测试文件 import MyComponent from './MyComponent'; import { render, fireEvent, screen } from '@testing-library/react'; test('能成功mock胖箭头的handleClick', () => { // 先造个mock函数 const mockHandleClick = jest.fn(); // 直接替换类上的handleClick MyComponent.prototype.handleClick = mockHandleClick; // 渲染组件 render(<MyComponent />); // 触发按钮点击 fireEvent.click(screen.getByText('点我呀')); // 验证mock被调用了 expect(mockHandleClick).toHaveBeenCalled(); });
原理就是:类实例化时,会优先用我们提前设置好的mock方法(取决于Babel的编译逻辑,这种方式基本稳)。
路子2:实例化后直接改实例上的方法
如果你已经把组件实例化了,那更直接的方式是拿到组件实例,直接替换它的handleClick:
// 用Enzyme的例子(Enzyme拿实例比RTL方便些) import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; test('直接在实例上mock handleClick', () => { const wrapper = shallow(<MyComponent />); const mockHandleClick = jest.fn(); // 直接替换实例上的handleClick wrapper.instance().handleClick = mockHandleClick; // 重点!shallow渲染的话,替换后要强制更新组件,让onClick绑定新的mock wrapper.update(); // 模拟点击 wrapper.find('button').simulate('click'); expect(mockHandleClick).toHaveBeenCalled(); });
这种方式完全绕开了原型链的问题,直接操作实例本身,稳得一批。
三、你的handleClick Mock失败的常见原因及解决办法
你遇到的Expected mock function to have been called错误,大概率是下面几种情况:
- 用了针对原型的spyOn,但handleClick是胖箭头方法:别再用
spyOn(MyComponent.prototype, 'handleClick')了,换成上面两种路子就行。 - 替换mock后没更新组件:尤其是shallow渲染时,组件的onClick绑定的是旧的方法引用,替换实例方法后必须调用
wrapper.update()让组件重新绑定事件。 - 事件触发错了元素:比如你触发的是自定义子组件,而不是最终渲染的原生button;或者用了错误的触发方式(比如RTL里没调用
fireEvent.click(),而是直接点元素)。 - handleClick在子组件里被封装了:如果按钮是子组件,比如
<CustomButton onClick={this.handleClick} />,那直接模拟子组件的点击可能不会触发父组件的方法,得找到原生button元素再触发。
额外小建议
如果你能改组件代码,其实更推荐把胖箭头方法改成普通类方法,然后在构造函数里绑定this,或者在render里用onClick={() => this.handleClick()},这样原型上的方法可以直接用jest.spyOn监视,测试起来更省心:
class MyComponent extends React.Component { constructor(props) { super(props); // 构造函数里绑定this this.handleClick = this.handleClick.bind(this); } handleClick() { // 你的逻辑 } render() { return <button onClick={this.handleClick}>点我呀</button> } }
这种写法下,测试时直接用const spy = jest.spyOn(MyComponent.prototype, 'handleClick')就可以正常监视了。
内容的提问来源于stack exchange,提问作者user6190673

