jest 测试 React 组件函数调用

问题

React 组件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';

class Sample extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
this.onClickButton1 = this.onClickButton1.bind(this);
}

onClickButton1() {
this.setState({
count: this.state.count + 1,
});
}

onClickButton2 = () => {
this.setState({
count: this.state.count + 1,
});
};

render() {
const { count } = this.state;
return (
<div>
<span>State: {count}</span>
<button className="button1" onClick={this.onClickButton1}>Button1</button>
<button className="button2" onClick={this.onClickButton2}>Button2</button>
</div>
);
}
}

export default Sample;

Sample 组件有两个方法分别是:onClickButton1()onClickButton2() 。虽然这两个方法都是将 statecount 加1,但是 onClickButton1 是 Sample 类的原型方法,而 onClickButton2 是 Sample 实例的属性方法

我们通过控制台可以看到:

1
2
3
4
5
6
7
8
9
console.log(Sample.prototype);
// Component {constructor: ƒ, onClickButton1: ƒ, render: ƒ}

console.log(new Sample().onClickButton2);
// ƒ () {
// _this.setState({
// count: _this.state.count + 1
// });
// }

在 Sample 的原型对象中是没有 onClickButton2 方法的。onClickButton2 方法必须实例化 Sample。在 Sample 类中直接写箭头函数在现在其实还是 ESnext 的 Class field declarations 提案,目前是 stage 3。如果要在 jest 中测试 Sample 组件中的这两个方法又应该如何测试呢。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import { mount } from 'enzyme';
import Sample from './Sample';

describe('<Sample />', () => {
it('should call onClickButton1() when click button1', () => {
const wrapper = mount(<Sample />);
const spy = jest.spyOn(Sample.prototype, 'onClickButton1');
wrapper.find('.button1').simulate('click');
expect(spy).toHaveBeenCalled();
});

it('should call onClickButton2() when click button2', () => {
const wrapper = mount(<Sample />);
const spy = jest.spyOn(wrapper.instance(), 'onClickButton2');
wrapper.find('.button2').simulate('click');
expect(spy).toHaveBeenCalled();
});
});

如果执行测试会发现两个测试均报错:

1
2
3
expect(jest.fn()).toHaveBeenCalled()

Expected mock function to have been called.

也就是说 jest 并没有监测到在点击相应按钮后调用了对应的方法。

解决

按钮的点击事件确实模拟了,通过判断 Sample 组件的 state 可以看到 count 确实加了 1。

1
2
3
4
5
it('should call onClickButton1() when click button1', () => {
const wrapper = mount(<Sample />);
wrapper.find('.button1').simulate('click');
expect(wrapper.state().count).toBe(1);
});

那么为什么没有监测到方法被调用了呢。通过查阅相关资料,是因为方法在调用前就被监测到了。这里对类的原型方法和属性方法的测试是有所区别的。

原型方法

类的原型方法的调用测试必须在对组件 shallowmount 之前先进行 spy

测试步骤如下:

  1. spy 原型方法
  2. shallow / mount 组件
  3. 模拟事件
  4. 测试

这里使用 shallow 或者 mount 都可以测试类的原型方法。

1
2
3
4
5
6
7
it('should call onClickButton1() when click button1 with prototype', () => {
const spy = jest.spyOn(Sample.prototype, 'onClickButton1');
const wrapper = shallow(<Sample />);
// const wrapper = mount(<Sample />);
wrapper.find('.button1').simulate('click');
expect(spy).toHaveBeenCalled();
});

属性方法

类的属性方法必须 mount 组件,并且在 spy 属性方法之后需要对 wrapperinstance() 执行 forceUpdate() 方法。

测试步骤如下:

  1. mount 组件
  2. spy wrapper 的实例方法
  3. forceUpdate() wrapper 的实例
  4. 模拟事件
  5. 测试

下面的代码可以通过测试。

1
2
3
4
5
6
7
it('should call onClickButton2() when click button2', () => {
const wrapper = mount(<Sample />);
const spy = jest.spyOn(wrapper.instance(), 'onClickButton2');
wrapper.instance().forceUpdate();
wrapper.find('.button2').simulate('click');
expect(spy).toHaveBeenCalled();
});

此外也可以通过 wrapper 的实例来测试类的原型方法。

1
2
3
4
5
6
7
it('should call onClickButton1() when click button1 with instance', () => {
const wrapper = mount(<Sample />);
const spy = jest.spyOn(wrapper.instance(), 'onClickButton1');
wrapper.instance().forceUpdate();
wrapper.find('.button1').simulate('click');
expect(spy).toHaveBeenCalled();
});

更多

如果在 Sample 组件中加入如下按钮:

1
<button className="button3" onClick={() => this.onClickButton2()}>Button3</button>

测试代码如下:

1
2
3
4
5
6
it('should call onClickButton2() when click button3', () => {
const wrapper = mount(<Sample />);
const spy = jest.spyOn(wrapper.instance(), 'onClickButton2');
wrapper.find('.button3').simulate('click');
expect(spy).toHaveBeenCalled();
});

这段测试将会通过,虽然这里没有执行 wrapper.instance().forceUpdate() 。但是通过了测试。这是因为 onClick 处是一个箭头函数,也就是说在每次组件 render() 时都会新建一个匿名函数来执行 onClickButton2() ,所以就监测到了 onClickButton2() 的执行。

render() 中绑定箭头函数会造成性能问题,因为每次 render 都建立了一个新的函数,并且也会导致不必要的组件重新 render

在线运行

可在实例查看测试效果


参考资料: