3. JavaScript 测试基础:断言库、测试用例结构与异步测试
好,咱们进入正题。这一章我打算聊聊 JavaScript 测试最核心的三个东西:断言库、测试用例结构、还有异步代码怎么测。这三个东西你搞明白了,后面写测试用例基本就顺了。
我记得我刚入行那会儿,写测试全靠 console.log 打印出来肉眼对比。后来被 senior 骂了一顿,才老老实实学断言库。嗯,真香。
3.1 断言库:expect 与 assert
断言是什么?说白了就是「我猜这个值应该是 xxx,如果不是,你就给我报错」。测试框架本身不负责断言,它只负责跑用例。断言这件事,得靠专门的库来做。
目前最主流的两个风格:assert(Node.js 内置)和 expect(Jest/Chai 等框架常用)。
3.1.1 assert 风格
Node.js 自带的 assert 模块,写法比较「老派」。你看一眼就明白了:
const assert = require('assert');
// 基本用法
assert.strictEqual(1 + 1, 2);
assert.notStrictEqual(1 + 1, '2');
// 对象比较
assert.deepStrictEqual({ a: 1 }, { a: 1 });
// 布尔判断
assert.ok(true);
assert.ok(1 === 1, '1 应该等于 1');
我个人习惯在写 Node.js 原生模块测试时用 assert,因为它零依赖。但说实话,它的报错信息不够友好。你想想看,一个 assert.strictEqual 失败了,就给你扔个「AssertionError」,连期望值和实际值都不给你列清楚。
== 和 === 容易搞混。建议始终用 strictEqual 和 deepStrictEqual,别用 equal 和 deepEqual。后者做的是弱比较,坑很多。
3.1.2 expect 风格
expect 风格就优雅多了。它用链式调用的方式,读起来像英语句子:
// Jest 或 Vitest 中的 expect
expect(1 + 1).toBe(2);
expect({ a: 1 }).toEqual({ a: 1 });
expect('hello').toHaveLength(5);
expect([1, 2, 3]).toContain(2);
// 布尔判断
expect(true).toBeTruthy();
expect(false).toBeFalsy();
// 异常判断
expect(() => {
throw new Error('出错了');
}).toThrow('出错了');
我在项目中遇到过一个问题:有人用 toBe 比较对象,结果永远失败。为什么?因为 toBe 用的是 Object.is,比较的是引用。对象比较得用 toEqual 或 toStrictEqual。
expect(x).toBeGreaterThan(0) 比 expect(x > 0).toBe(true) 报错信息清晰得多。
3.2 测试用例结构:describe 与 it
测试用例怎么写?核心就两个东西:describe 用来分组,it 用来写具体的测试。有些框架里 it 也叫 test,一回事。
3.2.1 基本结构
describe('计算器模块', () => {
describe('加法功能', () => {
it('应该正确计算两个正数相加', () => {
expect(add(1, 2)).toBe(3);
});
it('应该正确处理负数相加', () => {
expect(add(-1, -2)).toBe(-3);
});
});
describe('减法功能', () => {
it('应该正确计算两个正数相减', () => {
expect(subtract(5, 3)).toBe(2);
});
});
});
你看,describe 可以嵌套。外层 describe 描述模块,内层 describe 描述具体功能。it 里面写一条条具体的测试。
我曾经见过一个项目,所有测试用例都平铺在一个文件里,没有 describe 分组。跑测试的时候,报错信息根本不知道是哪个模块出了问题。嗯,后来重构花了两天。
3.2.2 生命周期钩子
测试框架还提供了几个钩子函数,用来做「前置准备」和「后置清理」:
| 钩子 | 执行时机 | 典型用途 |
|---|---|---|
beforeAll |
当前 describe 中所有测试之前,执行一次 | 初始化数据库连接、加载配置 |
afterAll |
当前 describe 中所有测试之后,执行一次 | 关闭数据库连接、清理临时文件 |
beforeEach |
每个 it 之前都执行一次 | 重置状态、创建测试数据 |
afterEach |
每个 it 之后都执行一次 | 清理测试数据、恢复 mock |
describe('用户服务', () => {
let userService;
beforeAll(() => {
// 只初始化一次
userService = new UserService();
});
beforeEach(() => {
// 每个测试前重置数据
userService.clear();
});
afterEach(() => {
// 每个测试后清理
console.log('测试完成');
});
it('应该能创建用户', () => {
userService.create('张三');
expect(userService.count()).toBe(1);
});
it('应该能删除用户', () => {
userService.create('李四');
userService.delete('李四');
expect(userService.count()).toBe(0);
});
});
3.3 同步与异步代码测试
前端开发,异步是家常便饭。回调、Promise、async/await,每种写法测试起来都不一样。
3.3.1 测试回调函数
早期的 Node.js 代码大量使用回调。测试回调函数,关键是要告诉框架「等异步操作完成再判断」:
// 被测函数
function fetchData(callback) {
setTimeout(() => {
callback('data');
}, 100);
}
// 测试用例
it('应该通过回调返回数据', (done) => {
fetchData((result) => {
expect(result).toBe('data');
done(); // 告诉框架,异步操作完成了
});
});
注意那个 done 参数。如果不调用 done,测试会超时失败。如果调用了两次 done,也会报错。
3.3.2 测试 Promise
Promise 的测试就简单多了。你直接 return 一个 Promise,框架会自动等待它 resolve:
// 被测函数
function fetchDataPromise() {
return new Promise((resolve) => {
setTimeout(() => resolve('data'), 100);
});
}
// 测试用例 - 方式1:return Promise
it('应该返回数据', () => {
return fetchDataPromise().then((result) => {
expect(result).toBe('data');
});
});
// 测试用例 - 方式2:使用 resolves
it('应该返回数据(更简洁)', () => {
return expect(fetchDataPromise()).resolves.toBe('data');
});
我个人更推荐第二种写法,resolves 和 rejects 让代码更简洁。测试 reject 的情况也一样:
// 被测函数
function fetchDataReject() {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('失败')), 100);
});
}
it('应该抛出错误', () => {
return expect(fetchDataReject()).rejects.toThrow('失败');
});
3.3.3 测试 async/await
async/await 是目前最主流的写法。测试起来也最自然:
// 被测函数
async function fetchDataAsync() {
return 'data';
}
// 测试用例
it('应该返回数据', async () => {
const result = await fetchDataAsync();
expect(result).toBe('data');
});
// 测试异常
it('应该抛出错误', async () => {
await expect(fetchDataReject()).rejects.toThrow('失败');
});
你想想看,async/await 的测试和同步代码几乎一样,只是多了个 await。这也是为什么现在大家都用 async/await 的原因——心智负担最小。
no-return-await 和 require-await 规则,帮你避免这种低级错误。
3.4 本章小结
这一章的内容其实就三句话:
- 断言:用 expect 或 assert 来验证结果。推荐 expect,可读性更好。
- 结构:用 describe 分组,用 it 写用例。别忘了 beforeEach 和 afterEach 做隔离。
- 异步:回调用 done,Promise 用 return,async/await 用 await。选 async/await 最省心。
下一章我会讲 mock 和 stub,那是测试里最实用的技巧之一。到时候我会分享一个我当年在项目中「mock 错了导致线上 bug」的惨痛教训。嗯,敬请期待。
公众号:蓝海资料掘金营,微信deep3321