主题
Jest
目标:把方法的行为在关键路径上完全覆盖 - 包括预期(正向)、异常/边界(负向)、可预见副作用(状态变更、异步状态)
使用
一、安装
bash
npm install --save-dev jest
# ts的类库(允许直接写xxx.test.ts)
npm install --save-dev jest ts-jest typescript @types/jest
# 生成ts配置文件
npx ts-jest config:init二、配置
json
"scripts": {
"test": "NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest --coverage"
}js
import { createDefaultEsmPreset } from 'ts-jest';
// 生成 ESM 模式下的默认配置
const tsJestTransformCfg = createDefaultEsmPreset().transform;
export default {
// 匹配测试文件
testMatch: ['**/__tests__/**/*.test.ts'],
// 测试环境:node or jsdom(浏览器环境)
testEnvironment: 'node',
// 支持 .ts 当作 ESM 模块
extensionsToTreatAsEsm: ['.ts'],
// 启用 ts-jest 的 ESM 转换
transform: {
...tsJestTransformCfg,
},
};三、创建__tests__文件夹
json
{
"compilerOptions": {
"target": "ES2019",
"module": "ESNext",
"declaration": true,
"declarationDir": "dist",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"lib": ["ES2019", "DOM"]
},
"include": ["src", "__tests__"], // 指定测试文件
"exclude": ["node_modules", "dist"]
}四、使用建议
- 先看契约(Spec)
- 方法参数允许哪些类型/范围?返回值的语义是什么?会抛错吗?会修改外部状态吗?
- 列出用例分类
- 正常路径
- 边界条件(空字符串、0、最大值、最小值、极大/极小数组长度)
- 非法输入(类型错误、null/undefined、不合法格式)
- 异常情况(外部依赖失败、网络超时)
结构
创建根目录文件夹
__test__,测试文件与代码文件名一一对应,后缀使用.test.ts
describe以模块/类/方法为单位it/test描述「输入 → 期望」,每个it只断言一个行为,test中可包含多个行为
js
// 模板
// foo.test.ts
import { sum, sub } from '../src/index';
describe('sum 函数', () => {
it('两个正数相加', () => expect(sum(1, 2)).toBe(3));
it('处理浮点数', () => expect(sum(0.1, 0.2)).toBeCloseTo(0.3, 5));
});
describe('sub 函数', () => {
beforeEach(() => {
/* reset mocks */
});
afterAll(() => {
/* 清理 */
});
it('正常返回', () => {
expect(sub(5, 3)).toBe(2);
});
});测试覆盖表
File文件路径% Stmts语句覆盖率% Branch分支覆盖率% Funcs函数覆盖率% Lines行覆盖率Uncovered Line #s未覆盖行号(没有被测试到的行号)
txt
PASS __tests__/a.test.ts
PASS __tests__/b.test.ts
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 94.44 | 100 | 80 | 100 |
index.cjs | 94.44 | 100 | 80 | 100 |
-----------|---------|----------|---------|---------|-------------------断言
基本数据类型
js
test('基本类型:number/string/boolean/null/undefined', () => {
expect(3).toBe(3); // 严格相等(===),适合原始类型
expect('hello').toBe('hello');
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// 浮点数比较(避免精度问题)
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
});引用数据类型
toEqual深度比较(适合对象/数组)toStrictEqual更严格(会检查对象的原型、不存在的属性、数组稀疏等)
js
test('对象与数组比较', () => {
expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 });
expect([1, 2, 3]).toEqual([1, 2, 3]);
// 严格比较(区分 undefined 属性以及类实例)
class A {
x = 1;
}
expect(new A()).toStrictEqual({ x: 1 }); // 视情况使用
});数组
数组包含、长度
js
test('数组相关', () => {
expect([1, 2, 3]).toContain(2);
expect(['a', 'b']).toHaveLength(2);
// 部分数组包含(order 不关心)
expect([1, 2, 3]).toEqual(expect.arrayContaining([1, 3]));
});对象
部分属性匹配、属性存在性
js
test('部分匹配与属性断言', () => {
const result = { id: 1, name: 'jack', meta: { age: 18 } };
// 只关心部分结构
expect(result).toMatchObject({ name: 'jack' });
// 断言有属性
expect(result).toHaveProperty('meta.age', 18);
});正则
js
test('字符串正则', () => {
expect('Hello World').toMatch(/World$/);
});异常
js
test('同步 throw', () => {
function bad() {
throw new Error('bad');
}
expect(() => bad()).toThrow('bad');
});
test('异步 reject', async () => {
await expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');
});Promise/async
js
test('异步 resolve', async () => {
await expect(Promise.resolve({ ok: true })).resolves.toEqual({ ok: true });
});