Jest 使用指南 - - Mock 篇

Jest 使用指南 - - Mock 篇

#jest

Jest Mock

为什么会用到 Mock? Mock 能帮我们解决什么问题?
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
- 擦除函数的实际实现(换句话说:改变函数的内部实现)
- 捕获函数调用情况( 包括:这些调用中传递的参数、new 的实例)
- 设置函数返回值

jest.fn()

Jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
Jest.fn()所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise对象。

test("测试jest.fn()返回固定值", () => {
    let mockFn = jest.fn().mockReturnValue("Felix");
    // 断言 mockFn 执行后返回值为Felix
    expect(mockFn()).toBe("Felix");
	  myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
	  // > 10 'x' true
});

test("测试jest.fn()内部实现", () => {
    let mockFn = jest.fn((x) => 42 + x);
    // 断言mockFn执行后返回 52
    expect(mockFn(10)).toBe(52);
});

test("测试jest.fn()返回Promise", async () => {
    let mockFn = jest.fn().mockResolvedValue("Felix");
    let result = await mockFn();
    // 断言mockFn通过await关键字执行后返回值为Felix
    expect(result).toBe("Felix");
    // 断言mockFn调用后返回的是Promise对象
    expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
});

jest.mock()

ES6 Class Mocks · Jest
有下面几种方式来 mock 模块

  1. 使用 jest.mock 自动 mock
    jest.mock('./utils.ts') 自动返回一个 mock ,可以使用它来监视对类构造函数及其所有方法的调用。方法调用保存在中
    theAutomaticMock.mock.instances[index].methodName.mock.calls
    ⚠️:如果您在类中使用箭头函数,则它们不会成为模拟的一部分。这样做的原因是,箭头函数不存在于对象的原型中,它们只是持有对该函数的引用的属性。
  2. jest.mock()直接在单元测试里面mock 模块
    jest.mock(path, moduleFactory) 接受模块工厂参数。模块工厂是一个返回模拟的函数。为了模拟构造函数,模块工厂必须返回构造函数。换句话说,模块工厂必须是返回函数的函数-高阶函数(HOF)。
    例如在 node 端会通过 fs 来读取文件,在单元测试中, 我们并不需要真去调用fs读取文件, 就可以考虑把fs模块mock掉, 如下代码:
jest.mock('fs', () => ({
    readFileSync: jest.fn()
}))

⚠️:这么使用有个限制就是,因为调用 jest.mock() 被提升到了文件开头,因此不可以先定义一个变量,然后在工厂中使用它。以 "mock "开头的变量是一个例外。这取决于你是否能保证它们按时被初始化!
3. 在需要mock的模块目录临近建立目录__mocks__
对于用户目录下面的模块
例如我们需要mock目录models下面的user模块,那么我们就需要在models下面新建__mocks__目录(这里要区分大小写),然后新建文件user.js。
注意:用这种方式, 需要在单元测试文件中需添加下面的代码才能使此mock生效。jest.mock('./moduleName')

// mock.ts
import utils from "./utils";
export default {
    test() {
        return utils.add(1, 2);
    },
};

// utils.ts
export default {
    add(a: number, b: number): number {
        console.log("----", a, b);
        return a + b;
    },
};

// mock.test.ts
import m from "./mock";
import utils from "./utils";
jest.mock("./utils.ts");
test("mock 整个 fetch.js模块", () => {
    m.test();
    expect(y).toBeCalledTimes(1);
});

终端结果
Jest 使用指南 - - Mock 篇

jest.spyOn()

Jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。

import m from "./mock";
import utils from "./utils";
test("mock 整个 fetch.js模块", () => {
    const y = jest.spyOn(utils, "add");
    m.test();
    expect(y).toBeCalledTimes(1);
});

上面 jest.mock() 终端截图中可以看到,console 并没有打印,这是因为通过jest.mock()后,模块内的方法是不会被jest所实际执行的。这时我们就需要使用jest.spyOn()。
Jest 使用指南 - - Mock 篇

Time mock

我们时常会用到 setTimeout 、setInterval、 clearTimeout 、clearInterval,针对这些定时器我们怎么去控制单测呢。
根据官方文旦time mock的说明,这里可以分为三种 Run All time(快进所有的定时时间)、Advance Timer by Time(快进指定的时间)、Run Pending Timer(针对嵌套了定时器的场景)

function timerGame(callback) {
  console.log('Ready....go!');
  setTimeout(() => {
    console.log("Time's up -- stop!");
    callback && callback();
  }, 1000);
}
module.exports = timerGame;

Run All Timer

jest.useFakeTimers();
test('calls the callback after 1 second', () => {
  const timerGame = require('../timerGame');
  const callback = jest.fn();
  timerGame(callback);
  // At this point in time, the callback should not have been called yet
  expect(callback).not.toBeCalled()
  // Fast-forward until all timers have been executed
  jest.runAllTimers();
  // Now our callback should have been called!
  expect(callback).toBeCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

注意:这里我们通过调用 jest.useFakeTimers(); 来启用假定时器。这用模拟函数模拟了 setTimeout 和其他计时器函数。

Advance TImer by Times

原先的方法名和上面的 runAllTimers 是呼应的,叫做 runTimersToTime,在 v22.0.0 后重命名为 advanceTimersByTime ,这个函数的作用就是快进指定的时间,

it('calls the callback after 1 second via advanceTimersByTime', () => {
  const timerGame = require('../timerGame');
  const callback = jest.fn();

  timerGame(callback);

  // At this point in time, the callback should not have been called yet
  expect(callback).not.toBeCalled();

  // Fast-forward until all timers have been executed
  jest.advanceTimersByTime(1000);

  // Now our callback should have been called!
  expect(callback).toBeCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

就上面这个而言,其实和 runAllTimer 是一样的,因为我们指定的时间就是全部的时间,下面我们可以尝试一下设置另外的时间


it("calls the callback after 999 millsecond via advanceTimersByTime", () => {
    const callback = jest.fn();
    timerGame(callback);
    expect(callback).not.toBeCalled();
	// 改为 999 毫秒,不到 1秒
    jest.advanceTimersByTime(999);
    expect(callback).not.toBeCalled();
    expect(callback).toHaveBeenCalledTimes(0);
	 // 又快进了 1 毫秒
    jest.advanceTimersByTime(1);
    expect(callback).toBeCalled();
    expect(callback).toHaveBeenCalledTimes(1);
});

上面的例子我们将指定时间改为。999 毫秒,不到 1 秒,所以相应的回调函数还没执行,根据运行结果的确是的,之后我们又只快进了 1毫秒,对应的 callback 执行了,符合我们的预期

最后一种 Run Pending Timer 就不多讲了,遇到了再来看,相对来说上面两种使用的场景较多

总结

在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试;jest.mock()可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要;当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。这些都需要开发者根据实际的业务代码灵活选择。

上一篇:Reactd组件库开发


下一篇:记一次前端vue3的单元测试之Hello world