测试模式指南
本文档介绍项目中常用的测试模式和最佳实践。
1. 临时目录测试模式
用于测试文件系统操作(如创建目录、写入文件等)。
使用场景
- 测试目录创建函数
- 测试文件写入操作
- 任何需要实际文件系统交互的测试
示例代码
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, afterEach } from "vitest";
import { ensureDir } from "./utils.js";
describe("ensureDir", () => {
let tmpDir: string;
// 测试后清理临时目录
afterEach(async () => {
if (tmpDir) {
await fs.promises.rm(tmpDir, { recursive: true, force: true });
}
});
it("creates nested directory", async () => {
// 创建系统临时目录
tmpDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "warelay-test-"),
);
// 构建嵌套路径
const target = path.join(tmpDir, "nested", "dir");
// 执行被测函数
await ensureDir(target);
// 验证目录已创建
expect(fs.existsSync(target)).toBe(true);
});
});
关键点
| 要点 | 说明 |
|---|---|
fs.promises.mkdtemp() | 创建唯一临时目录,避免命名冲突 |
os.tmpdir() | 获取系统临时目录路径 |
afterEach 清理 | 测试后删除临时目录,防止资源泄漏 |
recursive: true | 递归删除目录及其内容 |
force: true | 忽略不存在的错误 |
注意事项
- 必须清理:测试结束后务必删除临时目录
- 唯一命名:使用
mkdtemp自动生成随机后缀 - 错误处理:使用
try/finally或afterEach确保清理执行
2. 假定时器测试模式
用于测试涉及时间延迟的功能,避免真实等待。
使用场景
- 测试
sleep或delay函数 - 测试超时逻辑
- 测试轮询机制
- 任何依赖
setTimeout/setInterval的代码
示例代码
import { describe, expect, it, vi } from "vitest";
import { sleep } from "./utils.js";
describe("sleep", () => {
it("resolves after delay using fake timers", async () => {
// 1. 启用假定时器
vi.useFakeTimers();
// 2. 启动异步操作(不会真实等待)
const promise = sleep(1000);
// 3. 快进时间
vi.advanceTimersByTime(1000);
// 4. 验证结果
await expect(promise).resolves.toBeUndefined();
// 5. 恢复真实定时器(必须!)
vi.useRealTimers();
});
});
关键点
| API | 作用 |
|---|---|
vi.useFakeTimers() | 启用假定时器,接管所有定时器函数 |
vi.advanceTimersByTime(ms) | 快进指定毫秒数 |
vi.useRealTimers() | 恢复真实定时器 |
执行流程
真实时间: 0ms ──────────────────────────────────────►
useFakeTimers()
↓
sleep(1000) 启动,但暂停执行
↓
advanceTimersByTime(1000)
↓ ← 时间瞬间跳到1000ms
sleep 完成,promise resolve
↓
useRealTimers() 恢复真实定时器
注意事项
- 必须恢复:测试结束后务必调用
useRealTimers() - promise 处理:先启动异步操作,再快进时间
- 多个定时器:可使用
runAllTimers()运行所有挂起的定时器
其他常用 API
// 快进到下一个定时器
vi.advanceTimersToNextTimer();
// 运行所有定时器直到完毕
vi.runAllTimers();
// 只运行当前挂起的定时器
vi.runOnlyPendingTimers();
3. 完整对比
| 特性 | 临时目录模式 | 假定时器模式 |
|---|---|---|
| 用途 | 文件系统操作测试 | 时间相关功能测试 |
| 资源类型 | 磁盘空间 | 时间 |
| 清理方式 | afterEach + fs.rm() | useRealTimers() |
| 执行速度 | 正常(涉及真实IO) | 极快(跳过真实等待) |
| 风险 | 磁盘泄漏 | 定时器污染其他测试 |
3. Mock Spy 测试模式
用于拦截和监视函数调用,验证函数被正确调用且不影响测试输出。
使用场景
- 测试
console.log/console.error等输出函数 - 验证函数被调用的次数和参数
- 阻止副作用(如网络请求、文件写入)
- 保持测试输出干净
示例代码
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { logVerbose, setVerbose } from "./globals.js";
describe("logVerbose", () => {
let logSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
// 拦截 console.log,阻止实际输出
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
});
afterEach(() => {
// 恢复原始 console.log
logSpy.mockRestore();
});
it("should log when verbose is enabled", () => {
setVerbose(true);
logVerbose("test message");
// 验证 console.log 被调用了一次
expect(logSpy).toHaveBeenCalledTimes(1);
// 验证调用参数包含 "test message"
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining("test message")
);
});
});
关键点
| API | 作用 |
|---|---|
vi.spyOn(object, "method") | 监视对象的某个方法 |
.mockImplementation(fn) | 替换原方法实现 |
.mockRestore() | 恢复原始方法 |
toHaveBeenCalledTimes(n) | 验证调用次数 |
toHaveBeenCalledWith(arg) | 验证调用参数 |
代码详解
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
| 部分 | 含义 |
|---|---|
vi | Vitest 提供的全局对象 |
spyOn(console, "log") | 监听 console.log 方法的调用 |
mockImplementation(() => {}) | 将原方法替换为空函数,阻止实际输出 |
执行流程
正常调用: console.log("msg") → 终端输出 "msg"
Mock 后: console.log("msg") → 被 logSpy 记录 → 无终端输出
↓
可通过 logSpy 验证:
- 调用次数
- 调用参数
- 调用顺序
注意事项
- 必须恢复:测试结束后务必调用
mockRestore() - spy vs mock:
spyOn只监听,mockImplementation才真正拦截 - 类型安全:使用
ReturnType<typeof vi.spyOn>获取正确类型
其他常用 API
// 验证至少被调用一次
expect(spy).toHaveBeenCalled();
// 验证最后一次调用参数
expect(spy).toHaveBeenLastCalledWith(arg);
// 验证第 N 次调用参数
expect(spy).toHaveBeenNthCalledWith(1, arg);
// 获取所有调用记录
spy.mock.calls; // [[arg1, arg2], [arg1, arg2], ...]
// 重置调用记录(不清除 mock)
spy.mockClear();
// 完全重置(包括 mock)
spy.mockReset();
4. 重试逻辑测试模式
用于测试重试机制,验证函数在失败后能够正确重试并最终成功。
使用场景
- 测试带重试功能的异步函数
- 验证重试次数和最终结果
- 测试错误恢复逻辑
- 任何需要多次尝试才能成功的操作
示例代码
import { describe, expect, it, vi } from "vitest";
import { retryAsync } from "./retry.js";
describe("retryAsync", () => {
it("retries then succeeds", async () => {
// 创建模拟函数,设置不同调用的返回值
const fn = vi
.fn()
.mockRejectedValueOnce(new Error("fail1")) // 第1次:失败
.mockResolvedValueOnce("ok"); // 第2次:成功
// 执行重试函数(最多3次,间隔1ms)
const result = await retryAsync(fn, 3, 1);
// 验证最终返回正确结果
expect(result).toBe("ok");
// 验证函数被调用了2次(第1次失败,第2次成功)
expect(fn).toHaveBeenCalledTimes(2);
});
});
关键点
| API | 作用 |
|---|---|
vi.fn() | 创建模拟函数 |
.mockRejectedValueOnce(error) | 设置某次调用返回 rejected promise |
.mockResolvedValueOnce(value) | 设置某次调用返回 resolved promise |
expect(result).toBe(value) | 验证最终返回值 |
expect(fn).toHaveBeenCalledTimes(n) | 验证函数被调用次数 |
代码详解
expect(result).toBe("ok");
expect(fn).toHaveBeenCalledTimes(2);
| 断言 | 作用 | 验证内容 |
|---|---|---|
expect(result).toBe("ok") | 验证返回值 | retryAsync 最终返回 "ok" |
expect(fn).toHaveBeenCalledTimes(2) | 验证调用次数 | 模拟函数被调用了 2次 |
执行流程
第1次调用 fn() → 抛出 Error("fail1") → 等待 1ms → 重试
第2次调用 fn() → 返回 "ok" → 成功,返回结果
↓
验证: result === "ok"
验证: fn 被调用 2 次
为什么这两行断言很重要?
这两行断言共同验证了 retryAsync 的核心功能:
- 第一行 - 确认重试机制最终能成功获取结果
- 第二行 - 确认确实发生了重试(如果只用1次就成功,可能是逻辑错误)
类比理解
想象你在打电话:
- 第一次拨打 → 占线(失败)
- 第二次拨打 → 接通(成功)
这两行断言就是验证:
- ✅ 最终通话成功
- ✅ 总共拨打了 2 次
注意事项
- 顺序重要:
mockRejectedValueOnce和mockResolvedValueOnce按顺序生效 - 次数匹配:确保
toHaveBeenCalledTimes与实际调用次数一致 - 边界测试:同时测试"全部失败"的场景(重试次数耗尽)
其他常用 API
// 每次都返回相同值
vi.fn().mockResolvedValue("ok");
// 指定第 N 次调用的返回值
vi.fn()
.mockResolvedValueOnce("first")
.mockResolvedValueOnce("second")
.mockResolvedValue("default"); // 第3次及以后
// 验证最后一次调用参数
expect(fn).toHaveBeenLastCalledWith(arg);
// 获取所有调用记录
fn.mock.calls; // [[arg1], [arg2], ...]
5. 模式对比总结
| 特性 | 临时目录模式 | 假定时器模式 | Mock Spy 模式 | 重试逻辑模式 |
|---|---|---|---|---|
| 用途 | 文件系统操作测试 | 时间相关功能测试 | 函数调用验证 | 重试机制测试 |
| 资源类型 | 磁盘空间 | 时间 | 函数调用 | 异步重试 |
| 清理方式 | afterEach + fs.rm() | useRealTimers() | mockRestore() | 无需清理 |
| 执行速度 | 正常(涉及真实IO) | 极快(跳过真实等待) | 极快 | 极快 |
| 风险 | 磁盘泄漏 | 定时器污染其他测试 | 方法未恢复 | 次数验证错误 |
6. 最佳实践总结
- 始终清理资源:无论是临时文件、假定时器还是 Mock,测试后都要恢复
- 使用
afterEach:确保即使测试失败也能执行清理 - 避免真实等待:用假定时器替代
sleep测试 - 隔离测试:每个测试使用独立的临时目录
- 验证调用:使用 Spy 验证关键函数被正确调用