针对单测,JS 通常使用 Jest
此外,为了模拟 Dom 和事件,还需要 React Testing Library 进行辅助测试
Jest 的安装
对于一个不具备单测能力的 React 项目(如 vite),可以通过以下方式安装并配置 Jest
# 安装依赖
npm install --save-dev jest @types/jest @jest/types
# 初始化 Jest 配置
npx jest --init
# Choose the test environment that will be used for testing: 如果要涉及 dom 的单测就选 yes;只涉及 node 的纯逻辑就选 no
# Which provider should be used to instrument code for coverage:选择 babel,因为它可以转 ES5,避免兼容性问题
# 增加 babel 对应配置
npm install --save-dev babel-jest @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
# 安装 ts-node
npm install ts-node --save-dev
# 如果碰到类似“找不到 jest-environment-jsdom”的报错,就自行安装一下对应的依赖
npm install jest-environment-jsdom --save-dev
在根目录创建一个 babel.config.cjs
用于配置 babel
// ./babel.config.cjs
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
["@babel/preset-react", { runtime: "automatic" }], // 自动导入 React,不然后续单测的开发会要求对 React 进行 import。
"@babel/preset-typescript",
],
};
配置额外的扩展名识别
因为 jest 不使用 webpack 等打包工具,因此不知道如何加载除了 js/jsx 之外的其他文件拓展名。所以需要加一个转换器
// jest.config.ts
export default {
// ... other config
transform: {
// ...
"^.+.(js|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
},
};
Svg mock 转换
Jest 无法识别 svg。所以要对它进行 mock,返回相同的输出结果
// jest.config.ts
export default {
// ... other config
transform: {
// ...
"^.+.svg$": "<rootDir>/svg-transform.js",
},
};
// ./svg-transform.js
export default {
process() {
return { code: "module.exports = {};" };
},
getCacheKey() {
return "svgTransform"; // SVG固定返回这个字符串
},
};
CSS 代理
由于 Jest 本身不知道如何处理不同扩展的文件,我们可以通过配置代理的方式,告知 Jest 将对此对象模拟为导入的 CSS 模块
npm install --save-dev identity-obj-proxy
// jest.config.ts
export default {
// ... other config
moduleNameMapper: {
".(css|less)$": "identity-obj-proxy", // 有使用 sass 需求的话可以把正则换成 ^\.(css|less|sass|scss)$
},
};
React Testing Library 的安装
安装相关依赖:
# @testing-library/jest-dom:用于 dom、样式类型等元素的选取
# @testing-library/react:提供针对 React 的单测渲染能力
# @testing-library/user-event:用于单测场景下事件的模拟。
npm install @testing-library/jest-dom @testing-library/react @testing-library/user-event --save-dev
为了能让 expect 适配 React Testing Library 提供的相关断言,需要全局导入一下 @testing-library/jest-dom
在根目录新建一个 jest-dom-setup.js
:
// jest_dom_setup.js
import "@testing-library/jest-dom";
然后将该文件配置到 jest.config.ts
中:
// jest.config.ts
export default {
// 字段意义:将指定的配置文件,在安装测试框架之后、执行测试代码本身之前运行
setupFilesAfterEnv: ["<rootDir>/jest-dom-setup.js"],
};
Jest 断言
一个最简单的单元测试长这样:
// ./src/App.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
// describe 表示一组分组,其中可以包括多组 test
describe("test", () => {
// test 用于定义单个的用例
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
}
常见断言场景
基础类型的比较
import React from "react";
test("examples for jest expect", () => {
// tobe
expect(1 + 1).toBe(2);
// not tobe
expect(1 + 1).not.toBe(3);
// 判断 Boolean
expect(true).toBe(true);
expect(true).toBeTruthy();
expect(false).toBeFalsy();
// 判断 undefined
expect(undefined).toBe(undefined);
expect(undefined).not.toBeDefined();
expect(undefined).toBeUndefined();
// 判断函数返回值
const test = () => {};
expect(test()).toBeUndefined();
// 针对浮点数比较,用上面的 api 会出问题。需要使用专门提供的 `toBeCloseTo`,用于判断对象和预期的精度是否足够接近
expect(0.2 + 0.1).toBeCloseTo(0.3);
});
引用类型的比较
import React from "react";
test("examples for jest expect", () => {
// 对于深拷贝或是属性完全相同的对象,需要使用 toEqual。toEqual 会深度递归对象的每一个属性,进行深度比较。只要原始值相同,就能通过断言
// toEqual 同样可以用于简单类型
const a = { name: "name", age: 1 };
const b = JSON.parse(JSON.stringify(a));
expect(a).not.toBe(b);
expect(a).toEqual(b);
});
数值比较
import React from "react";
test("examples for jest expect", () => {
// >
expect(3).toBeGreaterThan(2);
// <
expect(3).toBeLessThan(4);
// >=
expect(3).toBeGreaterThanOrEqual(3);
// <=
expect(3).toBeLessThanOrEqual(3);
});
正则匹配
import React from "react";
test("examples for jest expect", () => {
// toMatch 会匹配字符串是否能满足正则的验证
expect("regexp validation").toMatch(/regexp/);
// toMatchObj 用于验证 value 是否是匹配对象的子集
const obj1 = { props1: "test", props2: "regexp validation" };
const obj2 = { props1: "test" };
// 由于 obj2 是 obj1 的子集,所以验证通过
expect(obj1).toMatchObj(obj2);
});
表单验证
import React from "react";
test("examples for jest expect", () => {
// toContain 判断某个值是否存在于数组中
expect([1, 2, 3]).toContain(1);
// arrayContaining 匹配接收的数组。此处和 toEqual 使用可以用于判定数组 [1, 2] 是否是数组 [1, 2, 3] 的子集
expect([1, 2, 3]).toEqual(expect.arrayContaining([1, 2]));
// toContainEqual 判定某个对象元素是否在数组中
expect([{ a: 1, b: 2 }]).toContainEqual({ a: 1, b: 2 });
// toHaveLength 断言数组长度
expect([1, 2, 3]).toHaveLength(3);
// toHaveProperty 断言对象中是否包含某个属性
// 针对多层级的对象,可以通过 xx.yy 的方式进行传参断言
const obj = {
props1: 1,
props2: {
props3: 2,
},
};
expect(obj).toHaveProperty("props1");
expect(obj).toHaveProperty("props2.props3");
});
错误抛出
import React from "react";
test("examples for jest expect", () => {
const throwError = () => {
const err = new Error("console err");
throw err;
};
expect(throwError).toThrow();
expect(throwError).toThrowError();
const catchError = () => {
try {
const err = new Error("console err");
throw err;
} catch (err) {
console.log(err);
}
};
expect(catchError).not.toThrow();
expect(catchError).not.toThrowError();
});
自定义断言
可以使用 Expect.extend
来自定义断言
同步的匹配器
e.g. 断言一个数字是否在 0-10 之间
test("0-10 sync test", () => {
const toBeBetweenZeroAndTen = (num: number) => {
if (num >= 0 && num <= 10) {
return {
message: () => "",
pass: true,
};
}
return {
message: () => "expect num to be a number between 0 and 10",
pass: false,
};
};
expect.extend({
toBeBetweenZeroAndTen,
});
expect(3).toBeBetweenZeroAndTen();
expect(13).not.toBeBetweenZeroAndTen();
});
异步的匹配器
test("0-10 async test", async () => {
const toBeBetweenZeroAndTen = async (num: number) => {
const res = await new Promise<{ message: () => string; pass: boolean }>(
(resolve) => {
setTimeout(() => {
if (num >= 0 && num <= 10) {
resolve({
message: () => "",
pass: true,
});
} else {
resolve({
message: () => "expected num to be a number between zero and ten",
pass: false,
});
}
}, 1000);
}
);
return (
res || {
message: () => "expected num to be a number between zero and ten",
pass: false,
}
);
};
expect.extend({
toBeBetweenZeroAndTen,
});
await expect(8).toBeBetweenZeroAndTen();
await expect(11).not.toBeBetweenZeroAndTen();
});