模拟函数

    Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new 实例化时捕获构造函数的实例、允许测试时配置返回值。

    有两种方法可以模拟函数:要么在测试代码中创建一个 mock 函数,要么编写一个手动 mock来覆盖模块依赖。

    假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。

    为了测试此函数,我们可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用。

    1. const mockCallback = jest.fn(x => 42 + x);
    2. forEach([0, 1], mockCallback);
    3. // 此 mock 函数被调用了两次
    4. expect(mockCallback.mock.calls.length).toBe(2);
    5. // 第一次调用函数时的第一个参数是 0
    6. expect(mockCallback.mock.calls[0][0]).toBe(0);
    7. // 第二次调用函数时的第一个参数是 1
    8. expect(mockCallback.mock.calls[1][0]).toBe(1);
    9. // 第一次函数调用的返回值是 42
    10. expect(mockCallback.mock.results[0].value).toBe(42);

    .mock 属性

    所有的 mock 函数都有这个特殊的 .mock属性,它保存了关于此函数如何被调用、调用时的返回值的信息。 .mock 属性还追踪每次调用时 this的值,所以我们同样可以也检视(inspect) this

    1. const myMock = jest.fn();
    2. const a = new myMock();
    3. const b = {};
    4. const bound = myMock.bind(b);
    5. bound();
    6. console.log(myMock.mock.instances);
    7. // > [ <a>, <b> ]

    这些 mock 成员变量在测试中非常有用,用于说明这些 function 是如何被调用、实例化或返回的:

    1. // 这个函数只调用一次
    2. expect(someMockFunction.mock.calls.length).toBe(1);
    3. // 这个函数被第一次调用时的第一个 arg 是 'first arg'
    4. expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
    5. // 这个函数被第一次调用时的第二个 arg 是 'second arg'
    6. expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
    7. // 这个函数被实例化两次
    8. expect(someMockFunction.mock.instances.length).toBe(2);
    9. // 这个函数被第一次实例化返回的对象中,有一个 name 属性,且被设置为了 'test’
    10. expect(someMockFunction.mock.instances[0].name).toEqual('test');

    Mock 函数也可以用于在测试期间将测试值注入代码︰

    1. const myMock = jest.fn();
    2. console.log(myMock());
    3. // > undefined
    4. myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
    5. console.log(myMock(), myMock(), myMock(), myMock());
    6. // > 10, 'x', true, true

    在函数连续传递风格(functional continuation-passing style)的代码中时,Mock 函数也非常有效。 以这种代码风格有助于避免复杂的中间操作,便于直观表现组件的真实意图,这有利于在它们被调用之前,将值直接注入到测试中。

    1. const filterTestFn = jest.fn();
    2. // Make the mock return `true` for the first call,
    3. // and `false` for the second call
    4. filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
    5. const result = [11, 12].filter(num => filterTestFn(num));
    6. console.log(result);
    7. // > [11]
    8. console.log(filterTestFn.mock.calls[0][0]); // 11
    9. console.log(filterTestFn.mock.calls[1][0]); // 12

    模拟模块

    假定有个从 API 获取用户的类。 该类用 调用 API 然后返回 data,其中包含所有用户的属性:

    users.js

    现在,为测试该方法而不实际调用 API (使测试缓慢与脆弱),我们可以用 jest.mock(...) 函数自动模拟 axios 模块。

    一旦模拟模块,我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。 In effect, we are saying that we want axios.get('/users.json') to return a fake response.

    users.test.js

    1. import axios from 'axios';
    2. import Users from './users';
    3. jest.mock('axios');
    4. test('should fetch users', () => {
    5. const users = [{name: 'Bob'}];
    6. const resp = {data: users};
    7. axios.get.mockResolvedValue(resp);
    8. // or you could use the following depending on your use case:
    9. // axios.get.mockImplementation(() => Promise.resolve(resp))
    10. return Users.all().then(data => expect(data).toEqual(users));
    11. });

    Subsets of a module can be mocked and the rest of the module can keep their actual implementation:

    foo-bar-baz.js

    1. export const foo = 'foo';
    2. export const bar = () => 'bar';
    3. export default () => 'baz';
    1. //test.js
    2. import defaultExport, {bar, foo} from '../foo-bar-baz';
    3. jest.mock('../foo-bar-baz', () => {
    4. const originalModule = jest.requireActual('../foo-bar-baz');
    5. //Mock the default export and named export 'foo'
    6. return {
    7. __esModule: true,
    8. ...originalModule,
    9. default: jest.fn(() => 'mocked baz'),
    10. foo: 'mocked foo',
    11. };
    12. });
    13. test('should do a partial mock', () => {
    14. const defaultExportResult = defaultExport();
    15. expect(defaultExportResult).toBe('mocked baz');
    16. expect(defaultExport).toHaveBeenCalled();
    17. expect(foo).toBe('mocked foo');
    18. expect(bar()).toBe('bar');
    19. });

    Mock 实现

    还有,在某些情况下用Mock函数替换指定返回值是非常有用的。 可以用 或 mockImplementationOnce方法来实现Mock函数。

    1. const myMockFn = jest.fn(cb => cb(null, true));
    2. myMockFn((err, val) => console.log(val));
    3. // > true

    foo.js

    1. module.exports = function () {
    2. // some implementation;
    3. };

    test.js

    当你需要模拟某个函数调用返回不同结果时,请使用 mockImplementationOnce 方法︰

    1. const myMockFn = jest
    2. .fn()
    3. .mockImplementationOnce(cb => cb(null, true))
    4. .mockImplementationOnce(cb => cb(null, false));
    5. myMockFn((err, val) => console.log(val));
    6. // > true
    7. myMockFn((err, val) => console.log(val));
    8. // > false

    mockImplementationOne定义的实现逐个调用完毕时, 如果定义了jest.fn ,它将使用 jest.fn

    1. const myMockFn = jest
    2. .fn(() => 'default')
    3. .mockImplementationOnce(() => 'first call')
    4. .mockImplementationOnce(() => 'second call');
    5. console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
    6. // > 'first call', 'second call', 'default', 'default'

    大多数情况下,我们的函数调用都是链式的,如果你希望创建的函数支持链式调用(因为返回了this),可以使用.mockReturnThis() 函数来支持。

    1. const myObj = {
    2. myMethod: jest.fn().mockReturnThis(),
    3. };
    4. // is the same as
    5. const otherObj = {
    6. myMethod: jest.fn(function () {
    7. return this;
    8. }),
    9. };

    你可以为你的Mock函数命名,该名字会替代 jest.fn() 在单元测试的错误输出中出现。 用这个方法你就可以在单元测试输出日志中快速找到你定义的Mock函数。

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

    自定义匹配器

    最后,测试Mock函数需要写大量的断言,为了减少代码量,我们提供了一些自定义匹配器。

    1. // The mock function was called at least once
    2. expect(mockFunc).toHaveBeenCalled();
    3. // The mock function was called at least once with the specified args
    4. expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
    5. // The last call to the mock function was called with the specified args
    6. expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
    7. // All calls and the name of the mock is written as a snapshot

    这些匹配器是断言Mock函数的语法糖。 你可以根据自己的需要自行选择匹配器。