Jest 模拟函数

2021-09-18 11:54 更新

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

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

使用模拟函数

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

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

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

  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​ 属性还追踪每次调用时 ​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> ]

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

  1. // The function was called exactly once
  2. expect(someMockFunction.mock.calls.length).toBe(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. // This function was instantiated exactly twice
  10. expect(someMockFunction.mock.instances.length).toBe(2);
  11. // The object returned by the first instantiation of this function
  12. // had a `name` property whose value was set to 'test'
  13. expect(someMockFunction.mock.instances[0].name).toEqual('test');

模拟的返回值

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

  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​)的代码中时,模拟函数也非常有效。 以这种代码风格有助于避免复杂的中间操作,便于直观表现组件的真实意图,这有利于在它们被调用之前,将值直接注入到测试中。

  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);
  9. // > [ [11], [12] ]

大多数现实世界例子中,实际是在依赖的组件上配一个模拟函数并配置它,但手法是相同的。 在这些情况下,尽量避免在非真正想要进行测试的任何函数内实现逻辑。

模拟模块

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

  1. // users.js
  2. import axios from 'axios';
  3. class Users {
  4. static all() {
  5. return axios.get('/users.json').then(resp => resp.data);
  6. }
  7. }
  8. export default Users;

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

一旦模拟模块,我们可为​ .get​ 提供一个 ​mockResolvedValue ​,它会返回假数据用于测试。 实际上,我们想让 ​axios.get(‘/users.json’)​ 有个假的 ​response​。

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

模拟实现

尽管如此,在某些情况下,超越指定返回值的能力并完全替换模拟函数的实现是有用的。这可以通过​jest.fn​或​mockImplementationOnce​模拟函数上的方法来完成。

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

mockImplementation

当你需要定义从另一个模块创建的模拟函数的默认实现时,该方法很有用:

  1. // foo.js
  2. module.exports = function () {
  3. // some implementation;
  4. };
  5. // test.js
  6. jest.mock('../foo'); // this happens automatically with automocking
  7. const foo = require('../foo');
  8. // foo is a mock function
  9. foo.mockImplementation(() => 42);
  10. foo();
  11. // > 42

当你需要重新创建模拟函数的复杂行为以致多个函数调用产生不同的结果时,请使用以下​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

当模拟函数用完用​mockImplementationOnce​定义的实现时,它将执行设置的默认实现​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​)的情况,我们有一个友好的 API 以​.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. };

模拟名称

你可以选择为你的模拟函数提供一个名称,该名称将在测试错误输出中显示而不是“jest.fn()”。如果你希望能够快速识别在测试输出中报告错误的模拟函数,请使用此选项。

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

自定义匹配器

最后,为了减少断言如何调用模拟函数的要求,我们为你添加了一些自定义匹配器函数:

  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
  8. expect(mockFunc).toMatchSnapshot();

这些匹配器是检查​.mock​财产的常见形式的糖。如果这更符合你的习惯或者你需要做一些更具体的事情,你始终可以自己手动执行此操作:

  1. // The mock function was called at least once
  2. expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
  3. // The mock function was called at least once with the specified args
  4. expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
  5. // The last call to the mock function was called with the specified args
  6. expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
  7. arg1,
  8. arg2,
  9. ]);
  10. // The first arg of the last call to the mock function was `42`
  11. // (note that there is no sugar helper for this specific of an assertion)
  12. expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
  13. // A snapshot will check that a mock was invoked the same number of times,
  14. // in the same order, with the same arguments. 它还会在名称上断言。
  15. expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
  16. expect(mockFunc.getMockName()).toBe('a mock name');

这些只是一部分,有关匹配器的完整列表,请查阅 参考文档


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号