模拟函数

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

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

    forEach.js

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

    forEach.test.js

    1. const forEach = require('./forEach');
    2. const mockCallback = jest.fn(x => 42 + x);
    3. test('forEach mock function', () => {
    4. forEach([0, 1], mockCallback);
    5. // The mock function was called twice
    6. expect(mockCallback.mock.calls).toHaveLength(2);
    7. // The first argument of the first call to the function was 0
    8. expect(mockCallback.mock.calls[0][0]).toBe(0);
    9. // The first argument of the second call to the function was 1
    10. expect(mockCallback.mock.calls[1][0]).toBe(1);
    11. // The return value of the first call to the function was 42
    12. expect(mockCallback.mock.results[0].value).toBe(42);
    13. });

    .mock 属性

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

    1. const myMock1 = jest.fn();
    2. const a = new myMock1();
    3. console.log(myMock1.mock.instances);
    4. // > [ <a> ]
    5. const myMock2 = jest.fn();
    6. const b = {};
    7. const bound = myMock2.bind(b);
    8. bound();
    9. console.log(myMock2.mock.contexts);
    10. // > [ <b> ]

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

    1. // The function was called exactly once
    2. expect(someMockFunction.mock.calls).toHaveLength(1);
    3. // The first arg of the first call to the function was 'first arg'
    4. expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
    5. // The second arg of the first call to the function was 'second arg'
    6. expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
    7. // The return value of the first call to the function was 'return value'
    8. expect(someMockFunction.mock.results[0].value).toBe('return value');
    9. // The function was called with a certain `this` context: the `element` object.
    10. expect(someMockFunction.mock.contexts[0]).toBe(element);
    11. // This function was instantiated exactly twice
    12. expect(someMockFunction.mock.instances.length).toBe(2);
    13. // The object returned by the first instantiation of this function
    14. expect(someMockFunction.mock.instances[0].name).toBe('test');
    15. // The first argument of the last call to the function was 'test'
    16. expect(someMockFunction.mock.lastCall[0]).toBe('test');

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

    1. console.log(myMock());
    2. // > undefined
    3. myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
    4. console.log(myMock(), myMock(), myMock(), myMock());
    5. // > 10, 'x', true, true
    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 ,它会返回假数据用于测试。 实际上,我们想说的是我们想让axios.get('/users.json') 有个伪造的响应结果。

    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. });

    模块的子集可以被模拟,模块的其他部分可以维持当前实现:

    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. const defaultExportResult = defaultExport();
    14. expect(defaultExportResult).toBe('mocked baz');
    15. expect(defaultExport).toHaveBeenCalled();
    16. expect(foo).toBe('mocked foo');
    17. expect(bar()).toBe('bar');
    18. });

    Mock 实现

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

    当你需要根据别的模块定义默认的Mock函数实现时,mockImplementation 方法是非常有用的。

    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. };

    You can optionally provide a name for your mock functions, which will be displayed instead of 'jest.fn()' in the test error output. Use if you want to be able to quickly identify the mock function reporting an error in your test output.

    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);

    匹配器的完整列表,请查阅 。