我使用jest测试项目中常见的问题

简介

公司的前端项目的技术栈为 react + ts + Rxjs。react就不多说了,大型项目一般都会采用react,单向数据流比双向绑定渲染来的更加准确和内存的更少消耗(diff)。ts的强类型保证了数据的精准,减少bug。Rxjs则会更好的获取异步数据,不受其他元素影响。技术的革新带来的是代码和效率的质变,同时也会对一部分技术带来冲击。比如说老牌单元测试框架jest。下面我就描述一下在项目中遇到的jest测试难题和解决过程,后面还有可能遇到新的会再更新到博文中

1. 对axios的mock

相信用jest写react单元测试的同学少不了要测试带有axios回调数据的组件。平时测试过程也较简单,mock一下axios即可。类似代码如下;

1
2
3
jest.mock('axios');
...
axios.get.mockResolvedValue(resp); /// resp为mock后的数据,

但是如果采用ts了会怎样??

what???ts的强类型导致代码的报错。于是开始在网上搜寻到以下解决答案:
1.将axios.get 别名。虽然不会报错,但是any类型很明显不是我们想要的,违背了ts的初始点

1
(axios.get as any).mockResolvedValue(resp);

2.利用jest.mock()的回调函数来满足。这里确实能够做到一定程度的mock,但是如果使用了axios的拦截器的话,那么就准备重写一个axios吧。这种方式不推荐。

1
2
3
jest.mock('axios',() =>{
get:() =>{} // 这里定义成想要的mock形态
})

我们无非就是要mock组件内的方法,使组件内的方法传出我们要模拟的值来做测试。所以我们可以使用jest.spyOn.顾名思义就是间谍我们的方法。

1
2
3
import axios from 'axios';
...
jest.spyOn(axios,'get').mockResolvedValue(resp);

ts无报错,又能很好的mock值,一举多得。如果我们自己写了服务文件,那么可以这么弄

1
2
3
import * as server from './Server';
...
jest.spyOn(server,'getData').mockResolvedValue(resp);

2.在Rxjs中断言

Rxjs是一个操作异步的库,里面有很多操作符,如map、pipe…操作符中我们不要用来断言,我们一般已经将结果mock掉了,所以直接在订阅subscribe中断言即可。但是我们对Rxjs的方法进行mock的时候还是需要注意的

1
2
3
4
import {of} from 'rxjs';
import * as server from './Server';
...
jest.spyOn(server,'getData').mockResolvedValue(of(resp));//这里需要用of来发送mock的数据

同时记得在订阅里的结尾,写好 done()表示结束。

3.受到withRouter影响

组件读取路由的参数,方法有很多,最好的莫过于使用‘react-router-dom’的withRouter这个高阶组件。但是这又会造成一个很大的问题,比如我们按照平时方式使用enzyme来mount组件

1
2
3
import App from './App';//这里App组件使用了withRouter
...
const wapper = mount(<App/>);

然而命令行提醒我们

这是我们就需要使用Router组件包裹一下

1
2
3
4
import App from './App';//这里App组件使用了withRouter
import {MemoryRouter} from 'react-router-dom';
...
const wapper = mount(<MemoryRouter><App/></MemoryRouter>);

此时感觉一切顺利,该怎么测试怎么测试。可是,如果我们的App组件中用到了componentWillReceiveProps这个生命周期的时候我们会发现,使用enzyme的setProps()方法不能将新的props注入到App的componentWillReceiveProps生命周期中。因为我们用setProps将参数注入到了MemoryRouter中了。于是我们可以封装一个新的class来进行测试

1
2
3
4
5
6
7
8
9
10
11
import App from './App';//这里App组件使用了withRouter
import {MemoryRouter} from 'react-router-dom';
class testApp extends React.Component<Iprops>{ // 这里的Iprops类型就是App的Props类型
render(){
return(
<MemoryRouter><App {...this.props}/></MemoryRouter>
)
}
}
...
const wapper = mount(<testApp />);

这里就可以看上去比较完美的解决componentWillReceiveProps这里的传参问题了。
可是新问题出来了,该怎么获取App组件内的state呢?或者说内部props?
我们重新看一下官网描述的mount()方法;

后面还可以带一个option配置。提供上下文配置,childContextTypes。于是我们找到router 的childContext相关类型,如下:

于是我们可以稍微整理一下option: (这里要感谢朱德龙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const option ={
childContextTypes: {
router: () => void 0, //包裹的router是函数类型
},
context: { // 上下文相关配置
router: {
history: createMemoryHistory(),
route: {
location: {
hash: '',
pathname: '',
search: '',
state: '',
},
match: { params: {}, isExact: false, path: '', url: '' },
}
}
}
}
...
const wapper = mount(<App/>,option);

现在,就可以各种操作App组件啦。得到state也好,更改props也好,按照enzyme上的文档操作即可。


作者简介: 张栓,人和未来大数据前端工程师,专注于html/css/js的学习与开发。