本文先用三个步骤,简单实现一下jest模拟接口请求。然后循序渐进,给出另一个更好的可选的方案。在阅读本文之前,你应当知晓如何使用jest。
先从一个例子开始,有个index.js
文件,使用axios
请求后端接口,代码如下:1
2
3
4
5
6
7
8
9// index.js
const axios = require('axios');
async function getFirstAlbumTitle() {
const response = await axios.get('https://jsonplaceholder.typicode.com/albums');
return response.data[0].title;
}
module.exports = getFirstAlbumTitle;
然后,我们要对它进行测试,index.test.js
代码如下:1
2
3
4
5
6
7// index.test.js
const getFirstAlbumTitle = require('./index');
it('returns the title of the first album', async () => {
const title = await getFirstAlbumTitle();
expect(title).toEqual('quidem molestiae enim');
});
如果不模拟接口请求,每次跑测试用例,都会真实地请求albums
接口,严重拖慢测试的速度。并且接口的响应值常常是不固定的,title并不一定会返回’quidem molestiae enim’,导致测试失败。
现在按照下列3个步骤:
- 在测试文件中导入需要模拟的模块
- 使用
jest.mock()
方法mock一下模块 - 使用.mockResolvedValue()模拟数据返回
将index.test.js
进行修改: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// index.test.js
const getFirstAlbumTitle = require('./index');
const axios = require('axios');
jest.mock('axios');
it('returns the title of the first album', async () => {
axios.get.mockResolvedValue({
data: [
{
userId: 1,
id: 1,
title: 'My First Album'
},
{
userId: 1,
id: 2,
title: 'Album: The Sequel'
}
]
});
const title = await getFirstAlbumTitle();
expect(title).toEqual('My First Album');
});
发生了什么?这里面最核心的一句是jest.mock('axios')
,这让jest
用一个空的函数,接管了axios
的所有行为,在没有使用mockResolvedValue
方法前,本文件中的axios
的所有方法都将返回undefined
:1
2
3
4
5
6
7
8
9
10
11const axios = require('axios');
jest.mock('axios');
// Does nothing, then returns undefined:
axios.get('https://www.google.com');
// Does nothing, then returns undefined:
axios.post('https://jsonplaceholder.typicode.com/albums', {
id: 3,
title: 'Album with a Vengeance'
});
注意,jest.mock()
应该在外层调用,而不是在it
中调用。
一旦调用了jest.mock('axios')
,在测试文件index.test.js
中的所有axio
请求都会被接管,包括在该文件中导入的getFirstAlbumTitle
中的axios
,其他测试文件不受影响。
至此,我们已经知道了如何模拟axios
请求,这种方案暂且命名为方案1。
方案1有个缺点,就是模拟的数据和测试用例杂糅在一起,如果模拟的数据过于庞大,远超过测试用例的代码量,这就有点混乱了,测试用例应该注重逻辑的书写,而不是人为模拟的数据。
接下来说的是方案2,讲的是如何把模拟的数据和测试用例分开。
在项目根目录下新建__mocks__
目录,大小写不能错,在__mocks__
目录下,新建一个和axios
齐名的js
文件axios.js
,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// axios.js
module.exports={
get:function(){
return new Promise(function(resolve){
resolve({
data: [
{
userId: 1,
id: 1,
title: 'My First Album'
},
{
userId: 1,
id: 2,
title: 'Album: The Sequel'
}
]
})
})
}
}
index.test.js
修改如下1
2
3
4
5
6
7
8
9
10// index.test.js
const getFirstAlbumTitle = require('./index');
const axios = require('axios');
jest.mock('axios');
it('returns the title of the first album', async () => {
const title = await getFirstAlbumTitle();
expect(title).toEqual('My First Album');
});
主要的变化是把axios.get.mockResolvedValue
替换成另外一种种实现形式,就是__mocks__
目录下的文件axios.js
。注意,方案2不适合模拟项目里自定义的接口请求模块。
为了模拟我们自己手写的封装好的自定义模块,只需要在和自定义模块的同一目录下,新建一个目录__mocks__
,然后在__mocks__
目录下新建一个和自定义模块齐名的js
文件就可以了,本质上和方案2相同,只是目录__mocks__
存放的位置不同。
举例,存在一个封装好接口请求的自定义模块,目录为moduls/request.js
,要对它的请求进行模拟,需要新建一个文件moduls/__mocks__/request.js
。
注意在调用jest.mock
模拟request.js
的时候,应当这样写:1
jest.mock('./moduls/request.js'); // 将文件目录作为参数传递给 jest.mock
而不是下面这种写法:1
2const request = require('./moduls/request.js');
jest.mock('request'); // 这样会让jest误以为模拟的是第三方模块 request
全文完。