写在前面:本文内容源于我自己在前端 TS 工具函数库项目中实践 Jest 的经验,并结合社区最佳实践整理而成。因为还在不断学习中,如果有不足之处,欢迎大家批评指正。
前言
前端开发中,TypeScript 工具函数库是很多项目的基础设施模块,比如常用的数据处理、日期处理、格式化、缓存等工具函数。如果没有良好的测试覆盖,这些基础功能出问题时,排查成本非常高。
Jest 是目前前端领域非常流行的单元测试框架,它不仅支持 TypeScript,也提供了 Mock、快照(Snapshot)、覆盖率(Coverage)统计等功能,非常适合工具函数库的测试。在这篇文章中,我将以我自己的工具函数库为例,完整分享如何从零搭建 Jest 单元测试环境,并附带最佳实践与实战示例。
一、为什么选择 Jest
在决定使用 Jest 之前,我曾对比过几个主流测试框架:
- Mocha + Chai:配置灵活,但需要自己选插件,TypeScript 支持需要额外配置。
- AVA:轻量快速,但社区生态相对小。
- Jest:开箱即用、TypeScript 支持完善、内置 Mock、覆盖率统计和 jsdom 环境,非常适合前端工具库。
经过实践,Jest 的优势非常明显:
- 内置 ts 支持,无需额外配置 Babel(使用 ts-jest 可以更高级)。
- 快照测试 功能非常适合 UI 组件,但工具函数库也可利用快照做 JSON 输出校验。
- 内置 Mock 功能,可以模拟全局对象,比如
localStorage、fetch。 - 覆盖率统计 友好,能直观看到函数覆盖率,方便优化测试用例。
因此,本篇文章围绕 Jest + TypeScript 的组合展开。
二、项目依赖安装
首先,我们来看看项目需要的依赖。我自己在工具库项目里使用了如下开发依赖:
"devDependencies": {
"@testing-library/jest-dom": "^6.4.5",
"@types/jest": "^28",
"@umijs/test": "^4",
"babel-jest": "^29.7.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"jest": "^28",
"jest-environment-jsdom": "^29.7.0",
"jsdom-global": "^3.0.2",
"jsdom-worker": "^0.3.0",
"mock-local-storage": "^1.1.24",
"ts-node": "^10",
"typescript": "~5.3.3"
}
然后在 package.json 中添加测试命令:
"scripts": {
"test": "jest",
"test:cov": "yarn test --collectCoverage"
}
小提示:我习惯在 CI 环境使用
test:cov来收集覆盖率,这样可以在 PR 时一目了然哪些函数没有被测试到。
三、Jest 配置详解
工具函数库一般没有 UI 依赖,但我们需要模拟浏览器环境(jsdom)、fetch、localStorage 等全局对象。下面是我整理的配置:
1. jest.config.ts
import { Config, createConfig } from '@umijs/test';
export default {
...createConfig(),
collectCoverageFrom: ['src/func/**/*.{ts,js,tsx,jsx}'],
testMatch: ['<rootDir>/test/**/*.spec.ts'],
setupFiles: ['mock-local-storage', 'jsdom-worker'],
setupFilesAfterEnv: ['<rootDir>/test/config/jest-setup.ts'],
clearMocks: true,
testEnvironment: './test/config/enhanceJsdomEnvironment.ts',
globals: {
fetch,
Request,
},
testTimeout: 30000,
coveragePathIgnorePatterns: [
'<rootDir>/src/func/workerHelper.ts'
],
} as Config.InitialOptions;
配置解读
collectCoverageFrom:指定需要收集覆盖率的文件。testMatch:指定测试文件匹配规则,我使用test/**/*.spec.ts。setupFiles:Jest 启动时先执行的文件,这里加载localStorage和jsdom-worker。setupFilesAfterEnv:在测试运行环境初始化后执行的文件,用于设置jest-dom。testEnvironment:自定义环境,增强了 jsdom 支持fetch等。globals:全局变量注入。testTimeout:设置单测超时时间,避免异步测试卡住。coveragePathIgnorePatterns:忽略部分文件的覆盖率统计,比如 Web Worker 文件。
2. test/config/jest-setup.ts
import '@testing-library/jest-dom';
import 'dotenv/config';
这里我加载了
jest-dom来增强 DOM 断言,以及dotenv读取环境变量。
3. test/config/enhanceJsdomEnvironment.ts
import JsdomEnvironment from 'jest-environment-jsdom';
export default class EnhanceJsdomEnvironment extends JsdomEnvironment {
constructor(...args: ConstructorParameters<typeof JsdomEnvironment>) {
super(...args);
this.global.fetch = fetch;
this.global.Headers = Headers;
this.global.Request = Request;
this.global.Response = Response;
}
}
通过继承
jsdom,我们可以在单测中使用原生fetch和 Request/Response 类,方便测试 API 调用。
4. test/utils/mock-storage.ts
global.window = {} as (Window & typeof globalThis);
import 'mock-local-storage';
window.localStorage = global.localStorage;
window.sessionStorage = global.sessionStorage;
解决 Node 环境下
localStorage不存在的问题。
5. test/utils/test-utils.ts
/**
* 等待指定的毫秒数
* @param ms - 等待的毫秒数
* @returns {Promise<void>}
*/
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export { wait };
实用工具函数,方便异步测试等待。
四、实战示例
假设我们的工具函数库有一个 math.ts,内容如下:
// src/func/math.ts
export function add(a: number, b: number) {
return a + b;
}
export function subtract(a: number, b: number) {
return a - b;
}
对应的单测写法:
// test/func/math.spec.ts
import { add, subtract } from '../../src/func/math';
describe('math utils', () => {
test('add should return correct sum', () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
});
test('subtract should return correct difference', () => {
expect(subtract(5, 2)).toBe(3);
expect(subtract(2, 5)).toBe(-3);
});
});
运行命令:
yarn test
输出:
PASS test/func/math.spec.ts
math utils
✓ add should return correct sum (3 ms)
✓ subtract should return correct difference
五、测试异步工具函数
假设我们有一个异步工具函数 delay.ts:
// src/func/delay.ts
export async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
对应测试:
// test/func/delay.spec.ts
import { delay } from '../../src/func/delay';
import { wait } from '../utils/test-utils';
describe('delay util', () => {
test('should resolve after specified ms', async () => {
const start = Date.now();
await delay(100);
const duration = Date.now() - start;
expect(duration).toBeGreaterThanOrEqual(100);
});
test('wait util should wait correct time', async () => {
const start = Date.now();
await wait(50);
const duration = Date.now() - start;
expect(duration).toBeGreaterThanOrEqual(50);
});
});
结合
wait工具函数可以更方便地测试异步逻辑。
六、Mock 全局对象
在工具库中,经常会操作 localStorage 或 fetch,Jest 提供了 Mock 功能:
// test/func/storage.spec.ts
describe('localStorage utils', () => {
beforeEach(() => {
localStorage.clear();
});
test('should set and get item', () => {
localStorage.setItem('key', 'value');
expect(localStorage.getItem('key')).toBe('value');
});
});
对于 fetch:
// test/func/fetch.spec.ts
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 123 }),
} as any)
);
test('fetch returns mocked data', async () => {
const res = await fetch('/api/test');
const data = await res.json();
expect(data).toEqual({ data: 123 });
});
Mock 可以让测试完全脱离网络依赖,提高稳定性。
七、覆盖率统计
运行:
yarn test:cov
输出示例:
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
math.ts | 100 | 100 | 100 | 100 | |
delay.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
覆盖率报告可以帮助我们发现未测试的边界条件或异常逻辑。
八、最佳实践分享
在长期实践中,我总结了几个最佳实践:
- 独立、纯函数优先:工具函数尽量无副作用,便于测试。
- 每个函数至少一个测试用例:覆盖正常、边界、异常三种情况。
- 使用 Mock 全局对象:避免依赖真实网络或浏览器 API。
- 覆盖率不等于质量:100% 覆盖率不代表测试充分,关注边界和异常逻辑。
- 按功能组织测试文件:保持
test/func与src/func一致。 - CI 集成:在 CI 中跑
yarn test:cov,保证 PR 不降低覆盖率。 - 异步测试要注意超时:尤其是网络或定时器相关逻辑。
- 善用工具函数:比如封装
wait、mockFetch,减少重复代码。
九、个人经验和反思
坦白讲,刚开始配置 Jest 的时候,我踩了很多坑:
- Node 环境下没有
window或localStorage。 - 异步测试超时导致 CI 失败。
- Web Worker 文件无法被 Jest 识别。
通过上面的配置,基本解决了这些问题,但仍有提升空间:
- 对 Worker 文件的覆盖率仍不完善。
- 对复杂依赖的 Mock 有时需要手动注入。
- 当工具函数涉及第三方 API 时,Mock 设计要仔细,否则测试不稳定。
总结一句:单测搭建是一件耐心活,稳扎稳打比追求花哨技巧更重要。
十、结语
本文分享了前端 TypeScript 工具函数库如何从零搭建 Jest 单元测试环境,包括:
- 依赖安装与配置
- jsdom 与全局对象 Mock
- 同步与异步函数测试
- 覆盖率统计与报告
- 最佳实践与经验分享
希望对正在尝试给工具库、前端基础库、或者小型项目搭建 Jest 单测的小伙伴们有所帮助。
作为一个还在不断学习的前端开发者,我很愿意把自己的实践经验分享出来,也希望大家在评论区交流踩坑心得,一起进步。
最后附上参考资料:
文章评论