【中文翻译】Jest模拟函数

原文 https://www.jestjs.cn/docs/mock-functions

Mock Functions

Mock 函数允许您通过删除函数的实际实现、捕获对该函数的调用(以及这些调用中传递的参数)、在使用 new 实例化时捕获构造函数的实例以及允许在测试时配置返回值来测试代码之间的链接。

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

Using a mock function

假设我们正在测试一个函数 forEach 的实现,该函数为所提供的数组中的每个项调用回调。

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

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

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

.mock property

所有模拟函数都有这个特殊的.mock 属性,用于保存关于函数如何被调用以及函数返回内容的数据。mock 属性也会跟踪每个调用的 this 值,所以也可以检查它:

const myMock = jest.fn();

const a = new myMock();
const b = {};
const bound = myMock.bind(b);
bound();

console.log(myMock.mock.instances);
// > [ <a>, <b> ]

这些模拟成员在测试中非常有用,可以断言这些函数是如何被调用、实例化的,或者它们返回了什么:

// 该函数只被调用过一次
expect(someMockFunction.mock.calls.length).toBe(1);

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

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

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

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

// 该函数的第一次实例化返回的对象
// 有一个' name '属性,其值被设置为'test'
expect(someMockFunction.mock.instances[0].name).toEqual("test");

Mock Return Values

Mock 函数也可以用于在测试期间向代码中注入测试值:

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

模拟函数在使用函数延续传递风格的代码中也非常有效。以这种风格编写的代码有助于避免对复杂存根的需求,这些存根会重新创建它们所代表的实际组件的行为,有利于在它们被使用之前直接向测试中注入值。

const filterTestFn = jest.fn();

// 让mock函数在第一次调用时返回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[0][1]); // 12

大多数实际示例实际上都涉及获得依赖组件上的模拟函数并对其进行配置,但技术是相同的。在这些情况下,尽量避免在没有直接测试的函数中实现逻辑。

Mocking Modules

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

// users.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);

  // 或者你可以根据你的用例使用以下方法:
  // axios.get.mockImplementation(() =比;Promise.resolve(resp))

  return Users.all().then((data) => expect(data).toEqual(users));
});

Mock Implementations

不过,在某些情况下,除了指定返回值和完全替换模拟函数的实现之外,还可以使用其他方法。这可以用玩笑来完成。fn 或 mock 函数的 mockImplementationOnce 方法。

const myMockFn = jest.fn((cb) => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

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

// foo.js
module.exports = function () {
  // some implementation;
};

// test.js
jest.mock("../foo"); // this happens automatically with automocking
const foo = require("../foo");

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

当你需要重新创建一个模拟函数的复杂行为,以便多个函数调用产生不同的结果时,使用 mockImplementationOnce 方法:

const myMockFn = jest
  .fn()
  .mockImplementationOnce((cb) => cb(null, true))
  .mockImplementationOnce((cb) => cb(null, false));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > false

当 mock 函数用完 mockImplementationOnce 定义的实现时,它将使用 jest 执行默认实现集。Fn(如果定义了):

const myMockFn = jest
  .fn(() => "default")
  .mockImplementationOnce(() => "first call")
  .mockImplementationOnce(() => "second call");

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

对于通常链接的方法(因此总是需要返回 this),我们有一个含糖的 API 以.mockReturnThis()函数的形式简化它,该函数也位于所有 mock 上:

const myObj = {
  myMethod: jest.fn().mockReturnThis()
};

// is the same as

const otherObj = {
  myMethod: jest.fn(function () {
    return this;
  })
};

Mock Names

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

const myMockFn = jest
  .fn()
  .mockReturnValue("default")
  .mockImplementation((scalar) => 42 + scalar)
  .mockName("add42");

Custom Matchers

最后,为了降低断言如何调用 mock 函数的要求,我们为您添加了一些自定义匹配器函数:

// 至少调用了一次mock函数
expect(mockFunc).toHaveBeenCalled();

// 使用指定的参数至少调用模拟函数一次
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

// 使用指定的参数调用了对模拟函数的最后一次调用
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

// 所有调用和模拟的名称都写为快照
expect(mockFunc).toMatchSnapshot();

这些匹配器是检查.mock 属性的常见形式的辅助工具。如果更符合你的口味,或者你需要做一些更具体的事情,你可以自己手动做:

// 至少调用了一次mock函数
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

// 使用指定的参数至少调用模拟函数一次
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);

// 使用指定的参数调用了对模拟函数的最后一次调用
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
  arg1,
  arg2
]);

//最后一次调用mock函数的第一个参数是' 42 '
//(注意,这个断言没有糖助手)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);

//快照将检查调用mock的次数是否相同,
//在相同的顺序,相同的参数。它还将断言名称。
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe("a mock name");

要查看匹配器的完整列表,请参阅参考文档