mock
单元测试一个不可或缺的部分,就是mock模拟,比如一些引入的第三方库、封装的一些utils方法、axios等,我们不关心它们内部的实现,只关心它们返回的值,就需要用到mock
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数的内部实现
具体用法
jest.fn()
模拟一个不用知道内部执行的方法
参考文档
jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值
it('Test', () => {
const testMockFn = jest.fn();
const result = testMockFn(1);
expect(testMockFn).toHaveBeenCalled();
// 函数没有返回值,调用后也应该是undefined
expect(result).toBeUndefined();
expect(testMockFn).toHaveBeenCalledTimes(1);
expect(testMockFn).toHaveBeenCalledWith(1);
expect(testMockFn).toHaveBeenLastCalledWith(1);
testMockFn(2);
expect(testMockFn).toHaveBeenCalledTimes(2);
expect(testMockFn.mock.calls).toHaveLength(2);
console.log(testMockFn.mock.calls);
// [ [ 1 ], [ 2 ] ]
});
只有间谍函数才能够使用toHaveBeenCalled来判断被调用,否则会报错如下:
mock函数的属性
.mock属性下的参数,该属性用于保存有关如何调用函数以及返回的函数的数据
- calls:二维数组,最外层数组长度表示被调用次数,内层数组表示调用的时候传入的参数,如下图:
- instance:当前this的指向
- invocationCallOrder:调用的顺序
- results:执行之后的返回值
jest.fn()还可以创建一个有返回值的的函数:
单纯返回值
it('设置返回值、设置内部实现', () => {
const mockFn = jest.fn().mockReturnValue(('test'));
expect(mockFn()).toBe('test');
})
采用链式调用,设置返回值的返回顺序
// 返回一次
const mockFnOnce = jest.fn().mockReturnValueOnce('once');
// 返回两次
// const mockFnOnce = jest.fn().mockReturnValueOnce('once').mockReturnValueOnce('seconde');
expect(mockFnOnce()).toBe('once');
// 第二次调用
expect(mockFnOnce()).toBeUndefined();
// 另外一种方式
const mockFnOther = jest.fn(() => {
return 2;
});
expect(mockFnOther()).toBe(2);
内部有逻辑:
it('内部有逻辑', () => {
// 设置内部实现--1,适用于简单函数
const mockFnInner = jest.fn((a, b) => {
return a + b;
});
expect(mockFnInner(1, 2)).toBe(3);
// 设置默认值
const mockFnDefault = jest.fn(() => {
return 'default'
}).mockReturnValueOnce(111);
console.log(mockFnDefault(), mockFnDefault(), mockFnDefault())
// 111, default,default
// 设置内部实现--2,适用于复杂函数(指定一个默认函数),或者是创建一个类
const mockFns = jest.fn().mockImplementation((a, b) => {
return a + b;
});
expect(mockFns(2, 2)).toBe(4);
});
返回promise
it('设置返回值、设置内部实现', async() => {
// 返回promise--1
const mockFnPromise = jest.fn(() => {
return Promise.resolve('resolve');
});
mockFnPromise().then(res => {
expect(res).toBe('resolve');
});
// 返回promise--2
const mockFnResolve = jest.fn().mockResolvedValue('default');
const result = await mockFnResolve();
expect(result).toBe('default');
})
模拟名称,在测试错误的时候,控制台输出模拟的名称而不是jest.fn()–没有用过
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');
jest.mock(moduleName,factory,options)
一般模拟的是一个模块而不是一个简单函数
参考文档
使用场景:
- 一个不需要经过测试的模块,比如说第三库(axios、oss),或者是测试覆盖率已经100%的模块
- 不关心内部实现,只需要结果的模块
比如模拟axios,我们可以完整模拟请求,通过对get、post等方法的重写,做到当测试用例发起请求时,被直接拦截,走我们自己设定的方法即可
参数解析:
- moduleName,模拟函数的路径或者是模块名字(第三方库)
- factory(可选),显示模拟当前模块的实现
- options(可选),是否是不存在的模块,{virtual: true}
在这里举例子模拟的是utils方法,axios模拟可以放到后面
import { commonFun } from '@/assets/js/utils/utils.js';
// 显示模拟commonFun文件里面的deepClone方法
jest.mock('@/assets/js/utils/utils.js', () => {
return {
commonFun: {
deepClone: jest.fn().mockImplementation((params) => {
return params;
})
}
}
})
// 模拟oss
jest.mock('ali-oss');
注意:
- 用jest模拟的模块,mock只针对调用jest.mock的文件进行模拟,导入该模块的另一个文件将获得原始实现
- 建议使用mockClear清除模拟,清除.mock.calls内容
Manual Mock
手动模拟
通过在紧邻模块的mocks/子目录中生成同名文件,从而编写模块来定义手动模拟
当你mock一个模块的时候,jest会自动去找当前文件紧邻的文件夹中是否有mock,如果有,再找mock文件夹下是否有该文件,如果有,则会自动覆盖当前测试用例的调用
├── __mocks__
│ └── fs.js
├── models
│ ├── __mocks__
│ │ └── user.js
│ └── user.js
注意:
mock文件夹区分大小写
**
模拟nodeModules里面下载的第三方模块
直接在tests文件夹的mock文件夹中定义同名文件,这个地方我的理解还有点模糊,仔细看看
其实也可以直接按照路径引入,这样是肯定不会有问题的,手动模拟的话,可维护性更高,复用性也更高
jest.doMock()
参考文档
jest.mock会自动将模拟提升到顶部,所以会作用到每一个用例中,用doMock不会提升
test('moduleName 1', () => {
jest.doMock('../moduleName', () => {
return jest.fn(() => 1);
});
const moduleName = require('../moduleName');
expect(moduleName()).toEqual(1);
});
test('moduleName 2', () => {
jest.doMock('../moduleName', () => {
return jest.fn(() => 2);
});
const moduleName = require('../moduleName');
expect(moduleName()).toEqual(2);
});
jest.spyOn(Object,methodName)
主要为了模拟一些必须被完整执行的方法
它是jest.fn的语法糖,实际上是创建了一个和被spyOn相同的函数,和执行真正的该方法相同
用法
// 另外一种方法
const commonFn = jest.spyOn(commonFun, 'deepClone');
await wrapper.vm.ossDownloadFile();
expect(commonFn).toHaveBeenCalled();
// 模拟console
const log = jest.spyOn(console, 'log');
- 我们想要测试一个方法里面,commonFun.deepClone是否被调用了
- 只能通过toHaveBeenCalled来判断
- 想要用toHaveBeenCalled来判断,那参数就必须是一个mock,并且我们想要测试深度克隆之后的代码
- jest.fn只能让函数成为mock,确定该方法被调用了,但是函数内部的执行却被替换了,所以需要使用spyOn
jest.spyOn(Object, methodName ,accessType)
jest从22.1.0+版本后,支持第三个参数,用来监听get和set方法 ```javascript // 官方例子 const video = { // it’s a getter! get play() { return true; }, };
module.exports = video;
const audio = { _volume: false, // it’s a setter! set volume(value) { this._volume = value; }, get volume() { return this._volume; }, };
module.exports = audio;
// test const audio = require(‘./audio’); const video = require(‘./video’);
test(‘plays video’, () => { const spy = jest.spyOn(video, ‘play’, ‘get’); // we pass ‘get’ const isPlaying = video.play;
expect(spy).toHaveBeenCalled(); expect(isPlaying).toBe(true);
spy.mockRestore(); });
test(‘plays audio’, () => { const spy = jest.spyOn(audio, ‘volume’, ‘set’); // we pass ‘set’ audio.volume = 100;
expect(spy).toHaveBeenCalled(); expect(audio.volume).toBe(100);
spy.mockRestore(); });
<a name="bn2cn"></a>
#### 覆盖监听
```javascript
const commonFn = jest.spyOn(commonFun, 'deepClone').mockImplementation(() => {
return '111'
});
mock清除
主要是清除mockFn.mock.calls和mockFn.mock.instances数组,数据调用顺序并没有被清除
整体清除:
jest.clearAllMocks()
jest.resetAllMocks()
// jest.spyOn有效
jest.restoreAllMocks()
单个清除:
// 适用于两个断言之间
mockFn.mockClear()
// 会删除任何模拟的返回值或实现
mockFn.mockReset()
mockFn.mockRestore()
区别
三个重要的api:jest.fn()、jest.spyOn()、jest.mock(),如下是区别:
jest.fn()
主要是模拟一个简单函数,不用知道内部执行的方法,可以设置返回值
jest.mock()
模拟某一个模块的所有方法,当某个模块已经测试覆盖率100%的时候,可以直接用mock进行模拟,可以节约测试时间和测试冗余度
jest.spyOn()
主要为了模拟一些必须被完整执行的方法,如果不重写方法,被模拟的模块是正常执行的,而以上两种模拟会拦截方法调用,根据模拟的函数来确定执行方法的逻辑