mock

单元测试一个不可或缺的部分,就是mock模拟,比如一些引入的第三方库、封装的一些utils方法、axios等,我们不关心它们内部的实现,只关心它们返回的值,就需要用到mock
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现

具体用法

jest.fn()
模拟一个不用知道内部执行的方法
参考文档
jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值

  1. it('Test', () => {
  2. const testMockFn = jest.fn();
  3. const result = testMockFn(1);
  4. expect(testMockFn).toHaveBeenCalled();
  5. // 函数没有返回值,调用后也应该是undefined
  6. expect(result).toBeUndefined();
  7. expect(testMockFn).toHaveBeenCalledTimes(1);
  8. expect(testMockFn).toHaveBeenCalledWith(1);
  9. expect(testMockFn).toHaveBeenLastCalledWith(1);
  10. testMockFn(2);
  11. expect(testMockFn).toHaveBeenCalledTimes(2);
  12. expect(testMockFn.mock.calls).toHaveLength(2);
  13. console.log(testMockFn.mock.calls);
  14. // [ [ 1 ], [ 2 ] ]
  15. });

只有间谍函数才能够使用toHaveBeenCalled来判断被调用,否则会报错如下:
image.png

mock函数的属性

.mock属性下的参数,该属性用于保存有关如何调用函数以及返回的函数的数据
image.png

  • calls:二维数组,最外层数组长度表示被调用次数,内层数组表示调用的时候传入的参数,如下图:
  • instance:当前this的指向
  • invocationCallOrder:调用的顺序
  • results:执行之后的返回值

image.png
jest.fn()还可以创建一个有返回值的的函数:
单纯返回值

  1. it('设置返回值、设置内部实现', () => {
  2. const mockFn = jest.fn().mockReturnValue(('test'));
  3. expect(mockFn()).toBe('test');
  4. })

采用链式调用,设置返回值的返回顺序

  1. // 返回一次
  2. const mockFnOnce = jest.fn().mockReturnValueOnce('once');
  3. // 返回两次
  4. // const mockFnOnce = jest.fn().mockReturnValueOnce('once').mockReturnValueOnce('seconde');
  5. expect(mockFnOnce()).toBe('once');
  6. // 第二次调用
  7. expect(mockFnOnce()).toBeUndefined();
  8. // 另外一种方式
  9. const mockFnOther = jest.fn(() => {
  10. return 2;
  11. });
  12. expect(mockFnOther()).toBe(2);

内部有逻辑:

  1. it('内部有逻辑', () => {
  2. // 设置内部实现--1,适用于简单函数
  3. const mockFnInner = jest.fn((a, b) => {
  4. return a + b;
  5. });
  6. expect(mockFnInner(1, 2)).toBe(3);
  7. // 设置默认值
  8. const mockFnDefault = jest.fn(() => {
  9. return 'default'
  10. }).mockReturnValueOnce(111);
  11. console.log(mockFnDefault(), mockFnDefault(), mockFnDefault())
  12. // 111, default,default
  13. // 设置内部实现--2,适用于复杂函数(指定一个默认函数),或者是创建一个类
  14. const mockFns = jest.fn().mockImplementation((a, b) => {
  15. return a + b;
  16. });
  17. expect(mockFns(2, 2)).toBe(4);
  18. });

返回promise

  1. it('设置返回值、设置内部实现', async() => {
  2. // 返回promise--1
  3. const mockFnPromise = jest.fn(() => {
  4. return Promise.resolve('resolve');
  5. });
  6. mockFnPromise().then(res => {
  7. expect(res).toBe('resolve');
  8. });
  9. // 返回promise--2
  10. const mockFnResolve = jest.fn().mockResolvedValue('default');
  11. const result = await mockFnResolve();
  12. expect(result).toBe('default');
  13. })

模拟名称,在测试错误的时候,控制台输出模拟的名称而不是jest.fn()–没有用过

  1. const myMockFn = jest
  2. .fn()
  3. .mockReturnValue('default')
  4. .mockImplementation(scalar => 42 + scalar)
  5. .mockName('add42');

jest.mock(moduleName,factory,options)

一般模拟的是一个模块而不是一个简单函数
参考文档
使用场景:

  • 一个不需要经过测试的模块,比如说第三库(axios、oss),或者是测试覆盖率已经100%的模块
  • 不关心内部实现,只需要结果的模块

比如模拟axios,我们可以完整模拟请求,通过对get、post等方法的重写,做到当测试用例发起请求时,被直接拦截,走我们自己设定的方法即可
参数解析:

  1. moduleName,模拟函数的路径或者是模块名字(第三方库)
  2. factory(可选),显示模拟当前模块的实现
  3. options(可选),是否是不存在的模块,{virtual: true}

在这里举例子模拟的是utils方法,axios模拟可以放到后面

  1. import { commonFun } from '@/assets/js/utils/utils.js';
  2. // 显示模拟commonFun文件里面的deepClone方法
  3. jest.mock('@/assets/js/utils/utils.js', () => {
  4. return {
  5. commonFun: {
  6. deepClone: jest.fn().mockImplementation((params) => {
  7. return params;
  8. })
  9. }
  10. }
  11. })
  12. // 模拟oss
  13. jest.mock('ali-oss');

注意:

  • 用jest模拟的模块,mock只针对调用jest.mock的文件进行模拟,导入该模块的另一个文件将获得原始实现
  • 建议使用mockClear清除模拟,清除.mock.calls内容

Manual Mock

手动模拟
通过在紧邻模块的mocks/子目录中生成同名文件,从而编写模块来定义手动模拟
当你mock一个模块的时候,jest会自动去找当前文件紧邻的文件夹中是否有mock,如果有,再找mock文件夹下是否有该文件,如果有,则会自动覆盖当前测试用例的调用

  1. ├── __mocks__
  2. └── fs.js
  3. ├── models
  4. ├── __mocks__
  5. └── user.js
  6. └── user.js

注意:
mock文件夹区分大小写

**

模拟nodeModules里面下载的第三方模块
直接在tests文件夹的mock文件夹中定义同名文件,这个地方我的理解还有点模糊,仔细看看
image.png
其实也可以直接按照路径引入,这样是肯定不会有问题的,手动模拟的话,可维护性更高,复用性也更高

jest.doMock()

参考文档
jest.mock会自动将模拟提升到顶部,所以会作用到每一个用例中,用doMock不会提升

  1. test('moduleName 1', () => {
  2. jest.doMock('../moduleName', () => {
  3. return jest.fn(() => 1);
  4. });
  5. const moduleName = require('../moduleName');
  6. expect(moduleName()).toEqual(1);
  7. });
  8. test('moduleName 2', () => {
  9. jest.doMock('../moduleName', () => {
  10. return jest.fn(() => 2);
  11. });
  12. const moduleName = require('../moduleName');
  13. expect(moduleName()).toEqual(2);
  14. });

jest.spyOn(Object,methodName)

主要为了模拟一些必须被完整执行的方法
它是jest.fn的语法糖,实际上是创建了一个和被spyOn相同的函数,和执行真正的该方法相同
用法

  1. // 另外一种方法
  2. const commonFn = jest.spyOn(commonFun, 'deepClone');
  3. await wrapper.vm.ossDownloadFile();
  4. expect(commonFn).toHaveBeenCalled();
  5. // 模拟console
  6. const log = jest.spyOn(console, 'log');
  1. 我们想要测试一个方法里面,commonFun.deepClone是否被调用了
  2. 只能通过toHaveBeenCalled来判断
  3. 想要用toHaveBeenCalled来判断,那参数就必须是一个mock,并且我们想要测试深度克隆之后的代码
  4. 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(); });

  1. <a name="bn2cn"></a>
  2. #### 覆盖监听
  3. ```javascript
  4. const commonFn = jest.spyOn(commonFun, 'deepClone').mockImplementation(() => {
  5. return '111'
  6. });

mock清除

主要是清除mockFn.mock.calls和mockFn.mock.instances数组,数据调用顺序并没有被清除
整体清除:

  1. jest.clearAllMocks()
  2. jest.resetAllMocks()
  3. // jest.spyOn有效
  4. jest.restoreAllMocks()

单个清除:

  1. // 适用于两个断言之间
  2. mockFn.mockClear()
  3. // 会删除任何模拟的返回值或实现
  4. mockFn.mockReset()
  5. mockFn.mockRestore()

区别

三个重要的api:jest.fn()、jest.spyOn()、jest.mock(),如下是区别:
jest.fn()
主要是模拟一个简单函数,不用知道内部执行的方法,可以设置返回值
jest.mock()
模拟某一个模块的所有方法,当某个模块已经测试覆盖率100%的时候,可以直接用mock进行模拟,可以节约测试时间和测试冗余度
jest.spyOn()
主要为了模拟一些必须被完整执行的方法,如果不重写方法,被模拟的模块是正常执行的,而以上两种模拟会拦截方法调用,根据模拟的函数来确定执行方法的逻辑