灏天阁

保姆级 Jest 教程

· Yin灏

jest 覆盖率

本文不适合纯白用户,建议先阅读 jest 的 Getting Started:jestjs.io/docs/gettin… 再来阅读本文,体感更好。

通过 jest --coverage 也叫 collectCoverage,该指令可以收集测试覆盖率信息并在输出中报告。 截图如下: image.png

  • %stmts 是语句覆盖率(statement coverage):是不是每个语句都执行了?
  • %Branch 分支覆盖率(branch coverage):是不是每个 if 代码块都执行了?
  • %Funcs 函数覆盖率(function coverage):是不是每个函数都调用了?
  • %Lines 行覆盖率(line coverage):是不是每一行都执行了?
  • Uncovered Line : 未覆盖到的行数

回调函数

异步场景

例如: 一个的业务场景:请求函数,请求成功后将数据放到callback中,进行数据处理。

demo 如下:

function fetchData(cb) {
  setTimeout(() => {
    cb("hello world");
  }, 1000);
}

测试代码

test("异步常景", (done) => {
  fetchData((params) => {
    try {
      expect(params).toBe("hello world"); // ok
      done();
    } catch (error) {
      done(error);
    }
  });
});
  1. 此时必须添加 done 作为回调的结束,如果不添加 done 函数,则会提示超时。 因为 fetchData 调用结束后,此测试就提前结束了。 并未等 callback 的调用。

    image.png

  2. 如果在测试过程中遇到不符合预期的情况,可以使用try catch将错误的信息传递给 done 的第一个参数。

  3. 如果不传递错误信息。遇到错误也可以通过测试,此时的测试结果是不准确的。

// 此测试用例也可以通过
test("callback", (done) => {
  fetchData((params) => {
    try {
      throw Error("error");
      done();
    } catch (error) {
      done();
    }
  });
});

同步回调

test("同步场景", () => {
  const fun = (cb) => {
    cb && cb();
  };
  const cbFun = jest.fn();
  fun(cbFun);
  expect(cbFun).toBeCalled(); // ok
});

异步函数

异步代码如下

function fetchData(type) {
  return type ? Promise.resolve("success") : Promise.reject("error");
}

测试逻辑

  1. 通过 promise 的方式

此处必须return ,把整个断言作为返回值返回。如果你忘了return语句的话,在  fetchData  返回的这个 promise 变更为 resolved 状态、then() 有机会执行之前,测试就已经被视为已经完成了

test("promise rejected", () => {
  return expect(fetchData(false)).reject.toBe("error");
});

test("promise resolve", () => {
  return expect(fetchData(true)).resolve.toBe("success");
});
  1. 通过 async / await 的方式
test("async resolve", async () => {
  let res = await fetchData(true);
  expect(res).toBe("success");
});

test("async reject", async () => {
  try {
    await fetchData(false);
  } catch (error) {
    expect(error).toMatch("error");
  }
});

注意:
expect.assertions(1) 推荐添加。如果 fetchData 不执行 reject,测试用例依旧可以通过,但是如果添加 expect.assertions(1) ,则要求此测试用例至少要被运行一次。即 必须 mock 一次 promise.reject的情况

try catch

demo 代码

const tryCatch = (data) => {
  try {
    data.push(77);
  } catch (error) {
    console.error("tryCatch", error.message);
    return null;
  }
};

测试逻辑

it("should throw an error if an exception is thrown", () => {
  // 使用 spyOn 进行模拟 console 方法,监听 console.error 方法
  const spy = jest.spyOn(console, "error");
  const result = tryCatch(123);
  // 测试返回值
  expect(result).toBeNull();
  // 传递的参数第一个是 tryCatch
  expect(spy).toHaveBeenCalledWith("tryCatch", expect.stringContaining(""));
  // 恢复原来 console.error 方法。(防止影响后续流程)
  spy.mockRestore();
});

jest.spyOn 方法可以替换模块中原有的方法。例

jest.spyOn(console, "error").mockImplementation(() => "this is error");
console.error(); // this is error

模拟-模块

模拟-本地模块

1. 方式一
假如我们要模拟一个 index.js 的模块。
首先在 index.js 文件同级目录新建一个名字为 __mocks__ 的文件夹,再创建一个名字与要模拟模块相同的文件。
例如:

├── __mocks__
│   └── index.js
└── index.js

Demo 示例

├── __test__
│   └── index.test.jsx
├── index.jsx
└── service
    ├── __mocks__
    │   └── index.js
    └── index.js
// index.jsx
import React from "react";
import localModule from "./service/index.js";

export const InputCom = () => {
  return <div>{localModule.name}</div>;
};
// service/index.js
module.exports = {
  name: "localModule",
};
// service/__mocks__/index.js
module.exports = {
  name: "mock localModule",
};
// index.test.js
import { render, screen } from "@testing-library/react";
import { LocalTest } from "..";
import React from "react";
import "../service";
jest.mock("../service");

test("测试本地模块", () => {
  render(<LocalTest></LocalTest>);
  screen.debug();
});

测试结果:

image.png

注意:有两个注意事项
1、该__mocks__文件夹区分大小写,因此__MOCKS__在某些系统上命名该目录会中断。 2、我们必须显式调用jest.mock('./moduleName')

2. 方式二

通过 jest.mock 方法,在文件内进行数据的模拟。不创建 __mocks__ 文件。

我们改写测试文件。

// index.test.js
import { render, screen } from "@testing-library/react";
import { LocalTest } from "..";
import React from "react";
import "../service";
jest.mock("../service", () => ({
  name: "mock localModule",
}));

test("测试本地模块", () => {
  render(<LocalTest></LocalTest>);
  screen.debug();
});

测试的结果与方式一一样。

模拟-第三方模块

1. 方式一

  • 我们在与 node_modules 同级目录下创建__mocks__
  • __mocks__下面创建我们需要模拟的第三方模块,格式:@scoped/moduled-name.js,例如: @testing/fun。 我们创建 __mocks__/@testing/fun.js 文件。
├── package.json
├── node_modules
├── index.js
├── __mocks__
│   └── @testing
│       └── fun.js
├── __test__
│   └── index.test.jsx
├── index.jsx
// index.jsx
import React from "react";
import nodeModule from "@testing/fun";

export const NodeModuleTest = () => {
  return <div>{nodeModule?.name}</div>;
};
// @testing/fun
module.exports = {
  name: "module-name",
  age: "module-age",
  fun: () => "module-fun",
};
// __mocks__/@testing/fun.js
module.exports = {
  name: "mock-nodeModule",
  fun: () => "mock-fun",
};
//index.test.js
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import NodeModuleTest from "..";
it("测试第三方模块", () => {
  render(<NodeModuleTest />);
  screen.debug();
});

运行测试用例以后,结果:
image.png

⚠️ 注意:
1、模拟第三方模块无须显示调用 jest.mock
2、Node 内置的模块,我们需要显示的调用 jest.mock 例如 fs ,我们需要手动的调用jest.mock(fs)

2. 方式二

//index.test.js
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import NodeModuleTest from "..";
jest.mock("@testing/fun", () => ({
  name: "mock-module-jest.mock",
}));
it("测试第三方模块", () => {
  render(<NodeModuleTest />);
  screen.debug();
});

运行测试用例以后的结果:

image.png

即使有__mocks__/@scoped/project-name.js 文件,在项目中的单独写的 jest.mock 方法权重最高。

我们模拟的时候后,可能有以下的诉求: 假设第三方包导出了 a,b,c 三个方法

  • b 方法使用真实方法,其他导出使用模拟的方法。
  • 仅模拟 a 方法,其他导出使用真是方法。

以上两种情况我们都可以使用 jest.requireActual() 来解决。

// index.jsx
import React from "react";
import nodeModule from "@testing/fun";

export const NodeModuleTest = () => {
  return (
    <div>
      {nodeModule?.name}
      {nodeModule?.age}
      {nodeModule?.sex}
    </div>
  );
};
// @testing/fun
module.exports = {
  name: "module-name",
  age: "module-age",
  sex: "module-sex",
};
//index.test.js
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import NodeModuleTest from '..'
jest.mock("@testing/fun", () => {
  const originalModule = jest.requireActual("@testing/fun");
  return {
    // __esModule: true, // 处理 esModules 模块
    ...originalModule,
    name: "mock-module-jest.mock",
  }
})
it("测试第三方模块", () => { render(<NodeModuleTest />) screen.debug(); });

image.png

函数测试

.mock 属性

在学习函数测试之前,我们需要先了解.mock属性都有哪些?
测试 demo 如下:

test(".mock", () => {
  const mockFun = jest.fn(() => {});
  console.log(mockFun.mock);
});

结果如下: image.png 实际上是 6 个属性,还有一个 lastCall 方法,这个方法只有在模拟的函数被调用的时候才会存在。

属性名称 属性含义
calls 包含对此模拟函数进行的所有调用的调用参数的数组。数组中的每一项都是调用期间传递的参数数组
contexts 包含模拟函数的所有调用的上下文的数组
instances 一个数组,其中包含已使用通过 new 模拟函数实例化的所有对象实例
invocationCallOrder 包含被调用的顺序
results 包含模拟函数的所有调用的上下文的数组
lastCall 包含对此模拟函数进行的最后一次调用的调用参数的数组。如果函数没有被调用,它将返回undefined

光看介绍可能不足以让我们了解到底是如何玩的,继续向下看:

  • calls

包含对此模拟函数进行的所有调用的调用参数的数组。数组中的每一项都是调用期间传递的参数数组

//mockProperty.test.js
test(".mock.calls", () => {
  const mockFun = jest.fn(() => {});
  mockFun(1);
  mockFun(2);
  console.log("%c Line:27 🍯 mockFun", "color:#b03734", mockFun.mock);
});

image.png 如上图所示,mockFun 方法调用了两次,分别传了1,2两个参数,打印结果中 calls 则是记录每次调用的入参。 即使调用的时候没有传参数, calls 依旧会记录调用的次数。

test(".mock.calls", () => {
  const mockFun = jest.fn(() => {});
  mockFun();
  mockFun();
  console.log("%c Line:27 🍯 mockFun", "color:#b03734", mockFun.mock.calls);
});

image.png

  • contexts

包含模拟函数的所有调用的上下文的数组

test(".mock.contexts", () => {
  const mockFn = jest.fn();
  const thisContext0 = () => {};
  const thisContext1 = () => {};
  const thisContext2 = () => {};
  const boundMockFn = mockFn.bind(thisContext0);
  boundMockFn();
  mockFn.call(thisContext1);
  mockFn.apply(thisContext2);
  console.log("%c Line:55 🍞 mockFn.mock", "color:#ea7e5c", mockFn.mock);
});

打印结果如下

image.png contexts 中记录了本次调用 this 所属上下文

  • instances

一个数组,其中包含已使用通过 new 模拟函数实例化的所有对象实例

test(".mock.instances", () => {
  const mockFn = jest.fn();
  const boundMockFn1 = new mockFn();
  const boundMockFn2 = new mockFn();
  const boundMockFn3 = new mockFn();
  console.log(
    "%c Line:55 🍞 mockFn.mock",
    "color:#ea7e5c",
    mockFn.mock.instances[0] == boundMockFn1
  ); // true
  console.log(
    "%c Line:55 🍞 mockFn.mock",
    "color:#ea7e5c",
    mockFn.mock.instances[1] == boundMockFn2
  ); // true
  console.log(
    "%c Line:55 🍞 mockFn.mock",
    "color:#ea7e5c",
    mockFn.mock.instances[2] == boundMockFn3
  ); // true
});
  • invocationCallOrder

包含被调用的顺序

image.png

使用上面的 instance 测试方法,打印结果 .mock 属性,可以在invocationCallOrder 方法中看到每个方法被调用的顺序。

  • results

包含模拟函数的所有调用的上下文的数组

用的比较多的属性。

  • 'return'- 表示调用正常返回完成。
  • 'throw'- 表示调用通过抛出值完成。
  • 'incomplete'- 表示呼叫尚未完成。如果您从模拟函数本身或从模拟调用的函数中测试结果,就会发生这种情况。
//type 为 return 的情况
test(".mock.result", () => {
  const mockFn = jest.fn(() => {
    return "";
  });
  mockFn(1);
  console.log(
    "%c Line:55 🍞 mockFn.mock.result",
    "color:#ea7e5c",
    mockFn.mock.results
  );
});

image.png

// type 为 throw
test(".mock.result", () => {
  const mockFn = jest.fn(() => {
    throw Error("error");
  });
  try {
    mockFn();
  } catch (error) {}
  console.log(
    "%c Line:55 🍞 mockFn.mock.result",
    "color:#ea7e5c",
    mockFn.mock.results
  );
});

image.png

// type 为incomplete
test(".mock.result", () => {
  const mockFn = jest.fn(() => {
    console.log(
      "%c Line:55 🍞 mockFn.mock.results",
      "color:#ea7e5c",
      mockFn.mock.results
    );
  });
  mockFn();
});

image.png

  • lastCall

包含对此模拟函数进行的最后一次调用的调用参数的数组。如果函数没有被调用,它将返回undefined

test(".mock.lastCall", () => {
  const mockFn = jest.fn(() => {});
  mockFn();
  mockFn(2);
  console.log(
    "%c Line:55 🍞 mockFn.mock.lastCall",
    "color:#ea7e5c",
    mockFn.mock.lastCall
  );
});

image.png

test(".mock.lastCall", () => {
  const mockFn = jest.fn(() => {});
  console.log(
    "%c Line:55 🍞 mockFn.mock.lastCall",
    "color:#ea7e5c",
    mockFn.mock.lastCall
  );
});

image.png

一个模拟函数f被调用了 3 次,返回'result1',抛出错误,然后返回'result2',将有一个mock.results如下所示的数组:

[
  {
    type: "return",
    value: "result1",
  },
  {
    type: "throw",
    value: {
      /* Error instance */
    },
  },
  {
    type: "return",
    value: "result2",
  },
];

模拟函数

  • 创建模拟函数
    模拟函数有三种方式
方法 介绍
jest.mock 模块的模拟
jest.fn jest.fn()是创建 Mock 函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回 undefined 作为返回值
jest.spyOn jest.fn 的语法糖,可以监控模拟函数的调用情况

三种方式都可以用来模拟函数。 jest.mock 不再做介绍.
jest.fn

onchange = jest.fn(); // 返回 undefined
// 可以模拟实现
onchange = jest.fn(() => console.log("log"));

jest.spyOn

let car = {
  stop: () => "stop",
};
jest.spyOn(car, "stop").mockImplementation(() => "mock-stop"); // mock-stop
  • 模拟函数返回

    • mockReturnValue

    函数的内容将不会被执行

    let mockFun = jest.fn().mockReturnValue("mockReturnValue");
    mockFun(); // mockReturnValue
    
    • mockReturnValueOnce
    let mockFun = jest
      .fn(() => "returnValue")
      .mockReturnValueOnce("mockReturnValue");
    mockFun(); // mockReturnValue
    mockFun(); // returnValue
    
    • mockResolvedValue
    let mockFun = jest
      .fn(() => "returnValue")
      .mockResolvedValue("mockReturnValue");
    mockFun(); // Promise { 'mockReturnValue' }
    
    • mockResolvedValueOnce
    let mockFun = jest
      .fn(() => "returnValue")
      .mockResolvedValue("mockReturnValue");
    mockFun(); // Promise { 'mockReturnValue' }
    mockFun(); // Promise { 'returnValue' }
    
    • mockRejectedValue
    let mockFun = jest
      .fn(() => "returnValue")
      .mockRejectedValue("mockReturnValue");
    await mockFun().catch((err) => {
      console.log(err); // mockReturnValue
    });
    
    • mockRejectedValueOnce
      同上

    除了单独使用还可以连在一起使用,例如:

    let mockFun = jest
      .fn(() => "returnValue")
      .mockReturnValue("default")
      .mockReturnValueOnce("first")
      .mockReturnValueOnce("two")
      .mockResolvedValueOnce("resolved")
      .mockRejectedValueOnce("rejected");
    
    mockFun(); // first
    mockFun(); // two
    mockFun(); // Promise { 'resolved' }
    mockFun(); // Promise { <rejected> 'rejected' }
    mockFun(); // default
    

测试

一般函数测试主要分为几种:

  • 测试函数的调用情况
  • 测试函数的返回值

我们在上面已经了解到了mockFun.mock属性,以及mockFun 的创建方式。下面进入本小节的核心,如何测试函数.

  • 函数调用 通过 .mocks.calls 属性的长度,判断函数是否被调用、调用的次数、调用的参数。

    • 函数是否被调用
    test(".mock.calls", () => {
      const mockFun = jest.fn(() => {});
      mockFun();
      expect(mockFun.mock.calls.length).toBe(1);
      mockFun();
      expect(mockFun.mock.calls.length).toBe(2); // ok
    });
    
    • 函数接收到的参数
    test(".mock.calls", () => {
      const mockFun = jest.fn(() => {});
      mockFun(1);
      mockFun();
      expect(mockFun.mock.calls[0][0]).toBe(1); // ok
      expect(mockFun.mock.calls[0][1]).toBeUndefined(); // ok
    });
    
  • 函数返回 通过 .mock.results 属性判断函数的结果

test(".mock.calls", () => {
  const mockFun = jest.fn((params) => params);
  mockFun(1);
  mockFun(2);
  expect(mockFun.mock.result[0].value).toBe(1); // ok
  expect(mockFun.mock.result[1].value).toBe(2); // ok
});

自定义匹配器

在测试一节,可以看到要判断一个函数的调用、返回相对比较麻烦,是否有简单的方式能够断言函数是否调用与返回呢?答案是有的,我们继续往下看:

  • 是否被调用 toBeCalled也叫做 toHaveBeenCalled
const fun = jest.fn();
expect(fun).toBeCalled(); // error
fun();
expect(fun).toBeCalled(); // ok
  • 调用次数 toHaveBeenCalledTimes
  • 调用参数 toHaveBeenCalledWith
  • 是否有返回值(没有抛出错误) toHaveReturned
  • 返回值 toHaveReturnedWith

定时器

API 作用
useFakeTimers 对文件中的所有测试使用假计时器,直到原始计时器恢复为jest.useRealTimers().
useRealTimers 恢复全局日期、性能、时间和计时器 API 的原始实现.
runAllTimers 执行所有挂起的宏任务和微任务.
runOnlyPendingTimers 仅执行当前挂起的宏任务(即,仅执行到此时或setInterval()之前已排队的任务)。如果任何当前挂起的宏任务安排新的宏任务,则这些新任务将不会被此调用执行。
advanceTimersByTime advanceTimersByTime(n) 所有计时器都会提前n毫秒

advanceTimersByTime

  • useFakeTimes

对文件中的所有测试使用假计时器,直到原始计时器恢复为jest.useRealTimers()

1、这是一个全局性的操作。无论是在测试文件的顶部还是某个测试 case 中。
2、会被替换的方法包括Dateperformance.now()queueMicrotask()setImmediate()clearImmediate()setInterval()clearInterval()setTimeout(),clearTimeout()
3、在 Node 环境中process.hrtimeprocess.nextTick()
4、在 JSDOM 环境中requestAnimationFrame(),cancelAnimationFrame()requestIdleCallback(),cancelIdleCallback()也会被替换。

function timerGame(callback) {
  console.log("Ready....go!");
  setTimeout(() => {
    console.log("Times up -- stop!");
    callback && callback();
  }, 1000);
}
const callback = jest.fn(() => "callback");
jest.spyOn(global, "setTimeout");
timerGame(callback);
expect(setTimeout).toHaveBeenCalledTimes(1); // ok
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); // ok
expect(callback).not.toBeCalled(); // ok

在此 demo 中,

  • 通过 spyOn 方法监听 setTimeout 方法 。
  • 通过 toHaveBeenCalledTimes 判断setTimeout方法是否被执行.

在这个例子中,我们只能确定setTimeout方法是否有被调用,但callback 方法是否在 1 秒之后调用我们并不知道,那么我们该如何确定我们的方法在 1 秒之后被调用了呢?就需要用到runAllTimers 方法,我们往下看。

  • runAllTimers

执行所有挂起的宏任务和微任务

补充上面的测试案例

jest.useFakeTimers();
const callback = jest.fn(() => "callback");
jest.spyOn(global, "setTimeout");
timerGame(callback);

expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

expect(callback).not.toBeCalled();

jest.runAllTimers();
expect(callback).toBeCalled(); // ok
  • runOnlyPendingTimers
function timerGame(callback) {
  console.log("Ready....go!");
  setTimeout(() => {
    console.log("Times up -- stop!");
    callback && callback();
    timerGame(callback);
  }, 1000);
}

如果我们的函数中存在递归调用的情况,在运行测试 case 的时候就会不断的执行。而我们的目标只要确定callback是否有被执行, 只需要测试一次即可。改写我们的测试 case 。

jest.useFakeTimers();
const callback = jest.fn(() => "callback");
jest.spyOn(global, "setTimeout");
timerGame(callback);

expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

expect(callback).not.toBeCalled();

jest.runOnlyPendingTimers();
expect(callback).toBeCalled(); // ok
  • advanceTimersByTime
jest.useFakeTimers();
const callback = jest.fn(() => "callback");
jest.spyOn(global, "setTimeout");
timerGame(callback);

expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

expect(callback).not.toBeCalled();

//jest.runAllTimers();
//jest.advanceTimersByTime();
expect(callback).toBeCalled(); // ok

在这里我们不再使用 runAllTimes,而是通过 advanceTimersByTime来提前执行callback

详细请看: jestjs.io/docs/jest-o…

- Book Lists -