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
相关产品推荐
相关产品推荐

