03 jest测试框架 -- mock

524 阅读4分钟

mock 函数

假设有个forEach函数,用来遍历一个数组,在遍历时候会调用callback, 我们想要知道callback的调用次数及调用参数是否符合预期,这时可以很方便的通过jest的函数模拟来实现。

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

为了测试这个forEach函数,我们可以使用mock函数模拟callback,并检查mock的状态,以确保按预期调用回调。

const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// mock函数被调用两次
expect(mockCallback.mock.calls.length).toBe(2);

// 第一次调用的第一个参数为0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// 第二次调用的第一个参数为1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// 第一次调用的返回值是42
expect(mockCallback.mock.results[0].value).toBe(42);

通过 jest.fn(implementation?)可以用来模拟函数,其中函数实现可有可无,根据需要设定即可。

如果只是判断调用次数和调用参数,没有函数实现也不影响

test('mock 函数', () => {
    // 仅仅创建一个空函数
    const mockFun = jest.fn()
    forEach([1,2],mockFun)
    expect(mockFun.mock.calls.length).toBe(2)
    expect(mockFun.mock.calls[0][0]).toBe(1)
});

.mock 属性

所以mock函数都有个特殊的.mock属性,记录函数如何被调用以及每次的返回值,也能追踪每次调用时的this。

const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]

const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ]

.mock的各种属性如下

// 函数调用次数
expect(someMockFunction.mock.calls.length).toBe(1);

// 第一次调用的第一个参数
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

// 第一次调用的第二个参数
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// 函数第一次调用的返回值
expect(someMockFunction.mock.results[0].value).toBe('return value');

// 函数第一次调用的上下文
expect(someMockFunction.mock.contexts[0]).toBe(element);

// 函数被实例化了两次
expect(someMockFunction.mock.instances.length).toBe(2);

// 第一次实例化对象的名称
expect(someMockFunction.mock.instances[0].name).toEqual('test');

// 函数最后一次调用的第一个参数
expect(someMockFunction.mock.lastCall[0]).toBe('test');

mock函数也可以通过函数匹配器来进行断言。

test('mock 函数', () => {
    const mockFun = jest.fn(x => x*2)
    forEach([1,2,3,3],mockFun)
    //被调用次数
    expect(mockFun).toBeCalledTimes(4)
    //被调用的参数有1
    expect(mockFun).toBeCalledWith(1)
    expect(mockFun).not.toBeCalledWith(4)
    //第4次调用参数为3
    expect(mockFun).toHaveBeenNthCalledWith(4,3)
    //最后一次调用参数为3
    expect(mockFun).toHaveBeenLastCalledWith(3)
    //有返回值
    expect(mockFun).toHaveReturned()
    //有返回值的有4次
    expect(mockFun).toHaveReturnedTimes(4)
    //返回值包含4
    expect(mockFun).toHaveReturnedWith(4)
    //最后一次返回值为6
    expect(mockFun).toHaveLastReturnedWith(6)
    //第3次返回值为6
    expect(mockFun).toHaveNthReturnedWith(3, 6)
});

mock函数返回值

除了mock一个函数用来断言函数的调用信息之外,有时我们想模拟一个函数的输出值, 比如第一次调用返回10,第二次调用返回'x',后续调用都返回true

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

在类似filter这种场景,通过设定函数返回值可以非常方便地筛选想要的数据。

const filterTestFn = jest.fn();

// 第一次返回true,第二次返回false
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterTestFn(num));

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

mock 模块(Modules)

假设我们有一个从API中获取用户信息的类。该类使用axios调用API,然后返回包含所有用户的数据属性。

user.js

import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

现在,为了测试这种方法而不实际影响API(如创建缓慢而脆弱的测试), 我们可以使用jest.mock(…)函数自动模拟axios模块。

一旦我们模拟了模块, 我们就可以为.get提供mockResolvedValue以返回我们希望的数据。 也就是说axios.get('/users.json')返回假响应。

users.test.js

import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);
  return Users.all().then(data => expect(data).toEqual(users));
});

模拟一部分

可以模拟模块的子集,模块的其余部分可以保留其实际实现:

foo-bar-baz.js

export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
  const originalModule = jest.requireActual('../foo-bar-baz');

  //Mock default 和 foo,原来的bar还保持不变
  return {
    __esModule: true,
    ...originalModule,
    default: jest.fn(() => 'mocked baz'),
    foo: 'mocked foo',
  };
});

test('should do a partial mock', () => {
  const defaultExportResult = defaultExport();
  expect(defaultExportResult).toBe('mocked baz');
  expect(defaultExport).toHaveBeenCalled();

  expect(foo).toBe('mocked foo');
  expect(bar()).toBe('bar');
});


这种方式对于模拟一些复杂的数据很有用,比如要测试的方法依赖Vuex,我们需要模拟store中的部分数据, 通过这种方式可很方便实现。

jest.mock('@/store/index', () => {
    return {
        state: {
            userInfo:{
            }
        }
    }
})

假设我们要测试的文件中也引用了"@/store/index",那么数据将会变成我们模拟的数据,而不必真正依赖vuex。

我正在参加「创意开发 投稿大赛」详情请看:[掘金创意开发大赛来了!](https://juejin.cn/post/7120441631530549284 "https://juejin.cn/post/7120441631530549284")