Vitest 异步测试实战:驯服 Promise 和 setTimeout 这两只野兽 🦁
前言:异步测试,程序员的噩梦?
异步编程就像是在玩杂技——你得同时抛接好几个球,还要确保它们不会砸到你的脑袋。而测试异步代码?那就是在杂技表演时还要闭着眼睛!😵💫
今天我们就来学学如何用 Vitest 这把"驯兽鞭",把 Promise 和 setTimeout 这两只调皮的野兽收拾得服服帖帖。
一、基础 Promise 测试:从小白兔开始 🐰
1.1 最简单的 Promise(连我奶奶都会)
先从最温顺的小白兔开始:
// index.js - 这个 Promise 乖得像只小绵羊
export function fetchUserData() {
return new Promise((resolve, reject) => {
resolve("1"); // 秒回,比微信还快
});
}
测试它就像撸猫一样简单:
// index.test.js
import { vi, it, expect, describe } from "vitest";
import { fetchUserData } from "./index";
describe("Promise", () => {
it("normal", async () => {
const result = await fetchUserData();
expect(result).toBe("1");
// 看!多简单,连 await 都不会反抗
});
});
划重点:
async/await是你的好朋友,别忘了它们- 测试函数必须标记为
async,不然就像开车不系安全带 🚗
1.2 Promise 链式调用:俄罗斯套娃的噩梦
现实项目中的 Promise 链就像俄罗斯套娃,一层套一层,让人头大:
// view.js - 这个类有点调皮
export class View {
count: number = 1;
render() {
// 看这个 Promise 链,像不像多米诺骨牌?
Promise.resolve()
.then(() => {
this.count = 2; // 第一张牌倒了
})
.then(() => {
this.count = 3; // 第二张牌也倒了
});
}
}
测试这货需要点技巧,不然你就等着被坑:
首先得安装这个神奇的小工具:
# npm 用户
npm install --save-dev flush-promises
# yarn 用户
yarn add -D flush-promises
# pnpm 用户
pnpm add -D flush-promises
flushPromises会等待前面所有的promise完成后才往下执行
// view.test.js
import { it, expect, describe } from "vitest";
import { View } from "./view";
import flushPromises from "flush-promises";
describe("View", () => {
it("should change count", async () => {
const view = new View();
view.render();
// 🎯 关键来了!等等,让子弹飞一会儿
await flushPromises();
expect(view.count).toBe(3);
});
});
为什么需要 flushPromises?让我用人话解释:
describe("Promise 的小脾气", () => {
it("❌ 新手常犯的错误(别学我)", () => {
const view = new View();
view.render();
// 这时候 Promise 还在偷懒,count 还是 1
expect(view.count).toBe(1); // 测试通过了,但这是假象!
console.log("我以为我赢了,其实我输得很彻底 😭");
});
it("✅ 正确的做法(学我这样)", async () => {
const view = new View();
view.render();
// 等待所有 Promise 执行完毕,就像等红绿灯
await flushPromises();
expect(view.count).toBe(3); // 现在才是真正的胜利!
console.log("这才是真正的王者 👑");
});
});
flushPromises 的作用:
- 它会等待所有微任务(microtask)执行完毕
- 就像给 Promise 链一个"全部执行完毕"的信号
- 没有它,你的测试就像在考试时交白卷 📝
二、setTimeout 测试:时间管理大师的秘密武器 ⏰
2.1 基础 setTimeout:等待是一种煎熬
// index.js - 这个函数有拖延症
export function delay(time: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("ok");
}, time);
});
}
如果按常规方法测试,你的测试套件会跑得比蜗牛还慢:
// ❌ 慢得让人怀疑人生的测试
describe("慢如蜗牛的测试", () => {
it("等待 1 秒,我都能泡杯茶了", async () => {
const result = await delay(1000);
expect(result).toBe("ok");
// 真的要等 1 秒!时间就是金钱啊!💰
});
});
2.2 Mock 时间:我是时间的主人!
用 Vitest 的假时间,你就是时间的主宰:
// index.test.js - 时间管理大师版本
import { vi, it, expect, describe } from "vitest";
import { delay } from "./index";
describe("Promise", () => {
it("delay", async () => {
// 🎭 戴上时间面具,我就是时间之神
vi.useFakeTimers();
// 启动定时器,但不等待
const result = delay(100);
// 时间快进!就像遥控器的快进键
vi.advanceTimersToNextTimer();
// 现在检查结果
expect(result).resolves.toBe("ok");
// 瞬间完成!比闪电侠还快 ⚡
});
});
关键技巧解析:
-
vi.useFakeTimers()- 开启时间控制模式vi.useFakeTimers(); // 我现在是时间的主人! -
vi.advanceTimersToNextTimer()- 跳到下一个定时器// 不管你设置多长时间,我一键跳过! vi.advanceTimersToNextTimer(); -
expect().resolves.toBe()- 测试 Promise 的最终结果// 不用 await,直接测试 Promise 会 resolve 成什么 expect(promise).resolves.toBe("expected value");
2.3 更多时间控制技巧
describe("时间管理大师的进阶技能", () => {
beforeEach(() => {
vi.useFakeTimers(); // 每次测试前都要戴上时间面具
});
afterEach(() => {
vi.useRealTimers(); // 测试后记得摘下面具,回到现实
});
it("精确控制时间流逝", async () => {
const promise1 = delay(100);
const promise2 = delay(200);
// 只过 100ms
vi.advanceTimersByTime(100);
await expect(promise1).resolves.toBe("ok");
// 再过 100ms,总共 200ms
vi.advanceTimersByTime(100);
await expect(promise2).resolves.toBe("ok");
console.log("时间在我手中就像橡皮泥 🕰️");
});
it("一键清空所有定时器", async () => {
const promises = [
delay(1000),
delay(2000),
delay(5000)
];
// 🚀 核弹级操作:一键清空所有定时器
vi.runAllTimers();
// 所有 Promise 都应该 resolve
for (const promise of promises) {
await expect(promise).resolves.toBe("ok");
}
console.log("定时器?在我面前都是弟弟!💪");
});
});
三、常见陷阱及避坑指南 🕳️
3.1 陷阱博物馆
describe("常见陷阱博物馆", () => {
it("❌ 陷阱1:忘记 await", async () => {
// 新手经常犯的错误
const promise = fetchUserData(); // 忘记 await
expect(promise).toBe("1"); // 这会失败!Promise 对象不等于字符串
// 正确做法
const result = await fetchUserData();
expect(result).toBe("1");
});
it("❌ 陷阱2:假时间忘记清理", () => {
vi.useFakeTimers();
// ... 测试代码 ...
// 如果忘记 vi.useRealTimers(),会影响其他测试!
});
it("❌ 陷阱3:Promise 链没等完", () => {
const view = new View();
view.render();
// 没有 await flushPromises()
expect(view.count).toBe(1); // 还没执行完呢!
});
});
3.2 最佳实践
describe("最佳实践指南", () => {
// ✅ 推荐:使用 beforeEach 和 afterEach
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks(); // 顺便清理所有 mock
});
it("✅ 完美的异步测试", async () => {
// 1. 设置场景
const view = new View();
const delayPromise = delay(1000);
// 2. 触发异步操作
view.render();
// 3. 控制时间流逝
vi.advanceTimersByTime(1000);
// 4. 等待所有异步操作完成
await flushPromises();
await expect(delayPromise).resolves.toBe("ok");
// 5. 验证结果
expect(view.count).toBe(3);
console.log("这就是完美的异步测试!✨");
});
});
四、实战技巧总结 📚
4.1 Promise 测试口诀
Promise 测试三字经:
- async 标记不能忘
- await 等待要用上
- flushPromises 清空忙
4.2 setTimeout 测试心法
setTimeout 测试心法:
- useFakeTimers 开启神功
- advanceTimersToNextTimer 时光飞行
- runAllTimers 一键清空
- useRealTimers 回到现实
4.3 工具箱清单
| 工具 | 用途 | 使用场景 |
|---|---|---|
async/await | 等待 Promise | 所有 Promise 测试 |
flushPromises | 清空微任务队列 | Promise 链测试 |
vi.useFakeTimers() | 开启假时间 | setTimeout 测试 |
vi.advanceTimersToNextTimer() | 跳到下一个定时器 | 单个定时器测试 |
vi.advanceTimersByTime(ms) | 精确推进时间 | 多个定时器测试 |
vi.runAllTimers() | 执行所有定时器 | 批量定时器测试 |
expect().resolves.toBe() | 测试 Promise 结果 | 不想用 await 时 |
五、总结:异步测试的武林秘籍 🥋
异步测试就像是在驯服两只野兽:
- Promise 这只狡猾的狐狸,喜欢躲在微任务队列里偷偷执行
- setTimeout 这只懒惰的熊,总是要睡一觉才干活
掌握了 Vitest 的这些技巧,你就是异步测试界的驯兽师!
最后的忠告:
- 测试不是为了证明代码没有 bug
- 而是为了在 bug 搞砸生产环境之前抓住它们!🐛
- 记住:好的测试就像保险,你希望永远用不到,但没有它你会睡不着觉 😴
愿你的测试永远绿色,愿你的异步代码永远可控! 💚
P.S. 如果你的测试还是红的,别慌,深呼吸,检查一下是不是忘记了 await 或者 flushPromises。异步测试就像学骑自行车,摔几次就会了!🚴♂️