示例说明
单元测试的第一原则是被测单元与外部世界隔离。vi.mock 的声明会被提升到文件顶部,导入解析时依赖已被替换——被测模块拿到的是替身而非真实网络客户端。vi.mocked 的类型包装让 mock 的设定与断言都有智能提示;beforeEach 清理保证用例之间零残留,执行顺序颠倒结果不变。
技术要点
- vi.mock 声明提升到导入之前生效
- vi.mocked 提供类型安全的桩设定
- clearAllMocks 保证用例间零残留
代码解读
工厂函数返回带 vi.fn 的替身对象;beforeEach 里重新设定默认汇率,个别用例按需覆盖为成功或拒绝。第三个用例断言调用次数验证调用行为本身——mock 的价值不仅在隔离,还在记录交互。
使用建议
mock 目标应是被测模块的依赖而非内部实现细节。过度 mock 让测试与实现耦合脆断。集成测试补齐真实边界,单元测试专注逻辑分支。
代码示例
import { describe, it, expect, vi, beforeEach } from 'vitest';
const rateClient = {
async getRate(currency: string): Promise<number> {
throw new Error('real network call in unit test');
},
};
vi.mock('./rate-client', () => ({
rateClient: { getRate: vi.fn() },
}));
import { priceInUsd } from './pricing';
import { rateClient } from './rate-client';
vi.mocked(rateClient.getRate).mockResolvedValue(7.2);
describe('priceInUsd', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(rateClient.getRate).mockResolvedValue(7.2);
});
it('converts amounts using the fetched rate', async () => {
vi.mocked(rateClient.getRate).mockResolvedValue(7.2);
const result = await priceInUsd(100, 'eur');
expect(result).toBeCloseTo(720);
expect(rateClient.getRate).toHaveBeenCalledWith('eur');
});
it('rejects when the rate service fails', async () => {
vi.mocked(rateClient.getRate).mockRejectedValue(new Error('fx down'));
await expect(priceInUsd(100, 'eur')).rejects.toThrow('fx down');
});
it('caches repeat lookups within one call', async () => {
await priceInUsd(50, 'eur');
await priceInUsd(60, 'eur');
expect(rateClient.getRate).toHaveBeenCalledTimes(2);
});
});