jest
jest 需要配合 git 使用,因为 jest 会检测 git 的提交记录,然后运行相关的测试用例。
运行 jest --watch 时,终端会有一下提示:
js
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
- 按
a运行所有测试用例 - 按
f只运行失败的测试用例,如果一开始成功,后来改失败了,不会运行 - 按
p用正则匹配文件名,只运行匹配到这个文件名的测试用例 - 按
t用正则匹配测试用例名,只运行匹配到这个名字的测试用例
异步测试
-
对于异步函数
fetchData的测试,jest默认不会等待异步测试用例执行完毕,所以需要在测试用例中传入一个done函数,然后在异步操作完成后调用这个函数goconst fetchData = (fn) => { fetch("http://localhost:8080/ping").then(async (response) => { if (response.ok) { const res = await response.json(); fn(res); } else { fn(response.status); } }); }; test("fetchData", (done) => { fetchData((data) => { expect(data).toEqual({ message: "pong" }); done(); }); }); -
修改异步函数
fetchData,返回一个Promise对象jsconst fetchData = () => { return new Promise((resolve, reject) => { fetch("http://localhost:8080/ping").then(async (response) => { if (response.ok) { resolve(await response.json()); } else { reject(response.status); } }); }); }; test("fetchData", () => { // 需要将 return 的 Promise 对象返回,否则 jest 会认为测试用例已经执行完毕 return fetchData().then((data) => { expect(data).toEqual({ message: "pong" }); }); }); -
测试
reject报错的情况,通过catch方法捕获错误,需要在测试用例中添加expect.assertions(1),因为异步请求响应正确的话catch分支不会被执行jstest("fetchData", () => { expect.assertions(1); return fetchData().catch((data) => { expect(data).toEqual(404); }); }); -
通过
toMatchObject方法判断jstest("fetchData", () => { return expect(fetchData()).resolves.toMatchObject({ message: "pong", }); }); -
测试
reject报错情况jstest("fetchData", () => { // 如果抛出的是 throw new Error(),则使用 toThrow // return expect(fetchData()).rejects.toThrow(); return expect(fetchData()).rejects.toEqual(404); });
mock
现在有个函数接收一个回调函数,并执行这个函数,测试用例应该怎么写呢?
js
const callback = (cb) => {
cb();
};
jest 提供了一个 jest.fn 的 mock 函数来测试
js
test("mock", () => {
const mockFn = jest.fn();
callback(mockFn);
expect(mockFn).toBeCalled();
});
mock 更高级的用法是,可以模拟请求的响应,在实际的测试中,我们不需要等待网络请求的响应,只需要模拟一个请求的响应即可
如果项目中使用 fetch 发起网络请求,需要使用下面的方法替代 fetch
js
global.fetch = jest.fn();
然后在测试用例中使用 mockImplementationOnce 或者 mockResolvedValue 模拟请求的响应
-
mockImplementationOncejsfetch.mockImplementationOnce(() => Promise.resolve({ json: () => Promise.resolve({ message: "pong" }), text: () => Promise.resolve("mocked text"), ok: true, status: 200, statusText: "ok", }) ); -
mockResolvedValuejsfetch.mockResolvedValue( Promise.resolve({ json: () => Promise.resolve({ message: "pong" }), text: () => Promise.resolve("mocked text"), ok: true, status: 200, statusText: "ok", }) );
js
test("mock", async () => {
fetch.mockImplementationOnce(() =>
Promise.resolve({
json: () => Promise.resolve({ message: "pong" }),
text: () => Promise.resolve("mocked text"),
ok: true,
status: 200,
statusText: "ok",
})
);
await fetchData().then((res) => {
expect(res).toEqual({ message: "pong" });
});
});
mock 高级用法
在当前目录下新建 __mocks__ 目录
新建文件 fetchData.js,写一个 fetchData 函数,然后在测试用例中引入这个函数
js
export const fetchData = () => {
return Promise.resolve({ message: "pong" });
};
在文件的开头用 jest.mock 引入这个文件,jest.mock 会自动到 __mocks__ 目录下找对应的文件(可以通过配置文件 jest.config.js 属性 automock 来配置)
import 导入的函数就会去 __mocks__ 目录下找对应的文件
js
jest.mock("./fetchData.js");
import { fetchData } from "./fetchData";
test("fetchData", () => {
return fetchData().then((data) => {
expect(data).toEqual({ message: "pong" });
});
});
如果想要引入真实 ./fetchData.js 文件中的函数,可以使用 jest.requireActual 方法
js
const {} = jest.requireActual("./fetchData.js");
snapshot
jest 提供了 snapshot 功能,可以将测试用例的结果保存到快照文件中,然后在下次测试时,将测试用例的结果与快照文件中的结果进行比较,如果不一致,则测试用例失败
jest 会在项目的根目下生成一个 __snapshots__ 文件夹,里面存放着测试用例的快照文件
测试不通过的话,按 u 键更新所有测试用例的快照文件,按 i 键一个个更新测试用例的快照文件
js
test("snapshot", () => {
expect(generateConfig()).toMatchSnapshot();
});
如果是个动态的数据,可以给 toMatchSnapshot 传入一个参数
js
expect(generateConfig()).toMatchSnapshot({
time: expect.any(Date),
});
定时器
定义一个 setTimeout 定时器,每隔 3 秒执行一次回调函数
js
export const timer = (cb) => {
setTimeout(() => {
cb();
setTimeout(() => {
cb();
}, 3000);
}, 3000);
};
我们在测试时,不需要等待 3 秒,可以使用 jest 提供的 jest.advanceTimersByTime 方法,来快速执行定时器
js
jest.useFakeTimers();
test("timer", () => {
const mockFn = jest.fn();
timer(mockFn);
jest.advanceTimersByTime(6000); // 快速执行 2 个定时器
expect(mockFn).toHaveBeenCalledTimes(2);
});
test("timer", () => {
const mockFn = jest.fn();
timer(mockFn);
jest.advanceTimersByTime(3000); // 快速执行 1 个定时器
expect(mockFn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(3000); // 再快速执行 1 个定时器
expect(mockFn).toHaveBeenCalledTimes(2);
});
jest 还有两个方法可以快速执行定时器:
jest.runAllTimers方法可以快速执行所有定时器jest.runOnlyPendingTimers方法可以快速执行队列中的待执行的定时器(只执行第一层的setTimeout)
jest 配置
-
collectCoverageFrom配置需要收集测试覆盖率的文件- 表示分析
src目录下的js、jsx、ts、tsx文件,但是排除src目录下的.d.ts文件
js{ collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"]; } - 表示分析
-
setupFiles配置在测试用例运行之前需要运行的文件js{ setupFiles: ["react-app-polyfill/jsdom"]; } -
setupFilesAfterEnv配置在测试用例运行之后需要做一些其他事情js{ setupFilesAfterEnv: ["<rootDir>/src/setupTests.js"]; } -
testMatch配置测试文件的匹配规则js{ testMatch: ["<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"]; } -
testEnvironment配置测试环境js{ testEnvironment: "jsdom"; } -
transform配置需要转换的文件- 当以
.js、.jsx、.mjs、.cjs、.ts、.tsx结尾时,使用babelTransform.js文件进行转换
js{ transform: { "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js", "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js", "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js" }; } - 当以
-
transformIgnorePatterns配置不需要转换的文件js{ transformIgnorePatterns: [ "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$", "^.+\\.module\\.(css|sass|scss)$", ]; } -
modulePaths配置模块路径 -
moduleNameMapper配置模块映射- 当引入
css module这样的文件时,identity-obj-proxy会返回一个对象,这个对象的属性名和属性值是一样的
js{ moduleNameMapper: { "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy", }, }; - 当引入
-
moduleFileExtensions配置模块文件扩展名js{ moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json", "node"]; } -
watchPlugins配置监听插件js{ watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"]; }