You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue+TypeScript导出工具函数单元测试最佳实践及Mock疑问

同模块方法Mock失效的原因与测试最佳实践

问题根源

你遇到的vi.spyOn监听同模块方法失效,核心原因是ES模块的导出绑定特性:当exportResults内部直接调用generateFileName和saveFile时,它调用的是模块内部的原始函数引用,而非你通过import获取的导出引用。vi.spyOn只能修改导出的函数引用,无法影响模块内部的直接调用。

解决方案

方案1:重构模块,通过自身引用调用内部方法

修改exportUtility.ts,让内部函数调用通过模块自身的导出引用进行,这样vi.spyOn就能捕获到调用:

// exportUtility.ts
import * as self from './exportUtility';

export interface ExportOptions {
  // 你的选项定义,比如:
  type: 'txt' | 'pdf';
  timestamp?: boolean;
}

export function generateFileName(options: ExportOptions): string {
  const suffix = options.type;
  const timestamp = options.timestamp ? `-${Date.now()}` : '';
  return `export${timestamp}.${suffix}`;
}

export function saveFile(content: string, fileName: string): void {
  // 示例实现:创建下载链接
  const blob = new Blob([content], { type: `text/${options.type}` });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = fileName;
  a.click();
  URL.revokeObjectURL(url);
}

export function exportResults(options: ExportOptions, content: string): void {
  // 改用模块自身引用调用依赖方法
  const fileName = self.generateFileName(options);
  self.saveFile(content, fileName);
}

测试代码可以正常使用vi.spyOn:

// exportUtility.test.ts
import * as exportUtils from './exportUtility';
import { vi, describe, it, expect, afterEach } from 'vitest';

describe('exportResults', () => {
  // 每个测试后恢复mock,避免用例污染
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('调用generateFileName生成文件名,并传入saveFile执行保存', () => {
    // Mock依赖方法
    const mockGenerate = vi.spyOn(exportUtils, 'generateFileName').mockReturnValue('test-file.txt');
    const mockSave = vi.spyOn(exportUtils, 'saveFile').mockImplementation(() => {});

    const testOptions = { type: 'txt', timestamp: false };
    const testContent = '测试内容';

    exportUtils.exportResults(testOptions, testContent);

    // 验证调用参数
    expect(mockGenerate).toHaveBeenCalledWith(testOptions);
    expect(mockSave).toHaveBeenCalledWith(testContent, 'test-file.txt');
  });
});

方案2:使用vi.mock部分Mock整个模块

如果不想修改原模块代码,可以用vi.mock直接Mock模块的导出,保留原始的exportResults逻辑:

// exportUtility.test.ts
import { vi, describe, it, expect, afterEach } from 'vitest';
import * as exportUtils from './exportUtility';

// 部分Mock:保留原始exportResults,Mock另外两个方法
vi.mock('./exportUtility', () => {
  const original = vi.importActual('./exportUtility');
  return {
    ...original,
    generateFileName: vi.fn(),
    saveFile: vi.fn()
  };
});

describe('exportResults', () => {
  afterEach(() => {
    vi.clearAllMocks();
  });

  it('正确传递参数给依赖方法', () => {
    (exportUtils.generateFileName as vi.Mock).mockReturnValue('report.pdf');
    (exportUtils.saveFile as vi.Mock).mockImplementation(() => {});

    const testOptions = { type: 'pdf', timestamp: true };
    const testContent = 'PDF测试内容';

    exportUtils.exportResults(testOptions, testContent);

    expect(exportUtils.generateFileName).toHaveBeenCalledWith(testOptions);
    expect(exportUtils.saveFile).toHaveBeenCalledWith(testContent, 'report.pdf');
  });
});

测试最佳实践

  • 单一职责原则:保持工具函数职责单一,比如generateFileName只负责生成文件名,saveFile只处理文件保存,exportResults只负责串联逻辑,这样每个函数的测试更简单,耦合更低。
  • 聚焦逻辑串联:测试exportResults时,只需要验证它是否正确调用了依赖方法、传递了正确参数即可,无需重复测试generateFileName和saveFile的内部逻辑(这两个方法已经单独测试过)。
  • 避免测试污染:使用afterEach钩子调用vi.restoreAllMocks()或vi.clearAllMocks(),确保每个测试用例的Mock状态是干净的。
  • Mock浏览器API:如果saveFile依赖浏览器API(如Blob、URL.createObjectURL),在测试中Mock这些API,避免测试依赖真实浏览器环境:
    // 示例:Mock URL.createObjectURL
    vi.spyOn(URL, 'createObjectURL').mockReturnValue('mock-blob-url');
    vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
    
  • 优先重构模块:方案1的重构不仅利于测试,还让模块内的依赖关系更清晰,后续维护更方便,是长期来看更优的选择。

内容的提问来源于stack exchange,提问作者Bastian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 23:47:35