← 中文资源

简体中文版本

Vitest 模块模拟单元测试

用 vi.mock 提升机制替换被测模块的依赖:类型化 mock 函数设定返回值,每个用例前清理状态保证测试顺序无关。

阅读英文原文

示例说明

单元测试的第一原则是被测单元与外部世界隔离。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);
  });
});