跳转到内容

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"]
}

四、使用建议

  1. 先看契约(Spec)
    • 方法参数允许哪些类型/范围?返回值的语义是什么?会抛错吗?会修改外部状态吗?
  2. 列出用例分类
    • 正常路径
    • 边界条件(空字符串、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 });
});

Will Try My Best.