保姆级 Jest 教程
jest 覆盖率
本文不适合纯白用户,建议先阅读 jest 的 Getting Started:jestjs.io/docs/gettin… 再来阅读本文,体感更好。
通过 jest --coverage
也叫 collectCoverage
,该指令可以收集测试覆盖率信息并在输出中报告。 截图如下:
%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);
}
});
});
-
此时必须添加
done
作为回调的结束,如果不添加done
函数,则会提示超时。 因为fetchData
调用结束后,此测试就提前结束了。 并未等callback
的调用。 -
如果在测试过程中遇到不符合预期的情况,可以使用
try catch
将错误的信息传递给 done 的第一个参数。 -
如果不传递错误信息。遇到错误也可以通过测试,此时的测试结果是不准确的。
// 此测试用例也可以通过
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");
}
测试逻辑
- 通过
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");
});
- 通过
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();
});
测试结果:
注意:有两个注意事项
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();
});
运行测试用例以后,结果:
⚠️ 注意:
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();
});
运行测试用例以后的结果:
即使有__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(); });
函数测试
.mock 属性
在学习函数测试之前,我们需要先了解.mock
属性都有哪些?
测试 demo 如下:
test(".mock", () => {
const mockFun = jest.fn(() => {});
console.log(mockFun.mock);
});
结果如下: 实际上是 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);
});
如上图所示,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);
});
- 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);
});
打印结果如下
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
包含被调用的顺序
使用上面的
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
);
});
// 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
);
});
// type 为incomplete
test(".mock.result", () => {
const mockFn = jest.fn(() => {
console.log(
"%c Line:55 🍞 mockFn.mock.results",
"color:#ea7e5c",
mockFn.mock.results
);
});
mockFn();
});
- 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
);
});
test(".mock.lastCall", () => {
const mockFn = jest.fn(() => {});
console.log(
"%c Line:55 🍞 mockFn.mock.lastCall",
"color:#ea7e5c",
mockFn.mock.lastCall
);
});
一个模拟函数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、会被替换的方法包括Date
,performance.now()
,queueMicrotask()
,setImmediate()
,clearImmediate()
,setInterval()
,clearInterval()
,setTimeout()
,clearTimeout()
。
3、在 Node 环境中process.hrtime
,process.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…