小龙虾学习基础知识7. dotenv 用法教程

5 阅读7分钟

测试模式指南

本文档介绍项目中常用的测试模式和最佳实践。

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/finallyafterEach 确保清理执行

2. 假定时器测试模式

用于测试涉及时间延迟的功能,避免真实等待。

使用场景

  • 测试 sleepdelay 函数
  • 测试超时逻辑
  • 测试轮询机制
  • 任何依赖 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(() => {});
部分含义
viVitest 提供的全局对象
spyOn(console, "log")监听 console.log 方法的调用
mockImplementation(() => {})将原方法替换为空函数,阻止实际输出

执行流程

正常调用: console.log("msg") → 终端输出 "msg"

Mock 后:  console.log("msg") → 被 logSpy 记录 → 无终端输出
                              ↓
                         可通过 logSpy 验证:
                         - 调用次数
                         - 调用参数
                         - 调用顺序

注意事项

  • 必须恢复:测试结束后务必调用 mockRestore()
  • spy vs mockspyOn 只监听,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. 第二行 - 确认确实发生了重试(如果只用1次就成功,可能是逻辑错误)

类比理解

想象你在打电话:

  • 第一次拨打 → 占线(失败)
  • 第二次拨打 → 接通(成功)

这两行断言就是验证:

  • ✅ 最终通话成功
  • ✅ 总共拨打了 2 次

注意事项

  • 顺序重要mockRejectedValueOncemockResolvedValueOnce 按顺序生效
  • 次数匹配:确保 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. 最佳实践总结

  1. 始终清理资源:无论是临时文件、假定时器还是 Mock,测试后都要恢复
  2. 使用 afterEach:确保即使测试失败也能执行清理
  3. 避免真实等待:用假定时器替代 sleep 测试
  4. 隔离测试:每个测试使用独立的临时目录
  5. 验证调用:使用 Spy 验证关键函数被正确调用