如何在Vitest中无需重建完整依赖树即可Mock服务/对象?
在Vitest中Mock类依赖的规范方案(替代Jasmine createSpyObj)
针对你遇到的Vitest中Mock类依赖的问题,下面提供几种无需构建完整依赖树的测试方案,同时满足测试用例中动态修改Mock返回值、调用未定义方法抛出错误的需求,以及对应的代码优化建议。
方案1:手动创建严格约束的Spy实例
直接模拟Jasmine createSpyObj的思路,手动构造仅包含被测代码所需方法的Mock对象,通过TypeScript类型约束和Proxy拦截确保未定义方法调用报错。
代码示例
假设被测服务和依赖结构如下:
// src/services/UserService.ts import { IApiClient } from './ApiClient'; export class UserService { constructor(private apiClient: IApiClient) {} async getUser(id: string) { return this.apiClient.get(`/users/${id}`); } } // src/services/ApiClient.ts export interface IApiClient { get(url: string): Promise<Record<string, any>>; } export class ApiClient implements IApiClient { constructor(private httpClient: HttpClient) {} // 无需关心这个依赖 get(url: string) { return this.httpClient.get(url); } }
测试代码:
// tests/UserService.test.ts import { describe, beforeEach, it, expect, vi } from 'vitest'; import { UserService } from '../src/services/UserService'; import type { IApiClient } from '../src/services/ApiClient'; // 创建严格Mock的工具函数:访问未定义属性时抛出错误 function createStrictMock<T>(mock: Partial<T>): T { return new Proxy(mock as T, { get(target, prop) { if (prop in target) return Reflect.get(target, prop); throw new Error(`Mock对象未定义属性/方法: ${String(prop)}`); } }); } describe('UserService', () => { let mockApiClient: IApiClient; let userService: UserService; beforeEach(() => { // 仅Mock被测代码用到的get方法 mockApiClient = createStrictMock({ get: vi.fn() }); userService = new UserService(mockApiClient); }); it('通过ID获取用户数据', async () => { // 测试用例中动态修改返回值 mockApiClient.get.mockResolvedValue({ id: '1', name: 'Mike' }); const user = await userService.getUser('1'); expect(mockApiClient.get).toHaveBeenCalledWith('/users/1'); expect(user).toEqual({ id: '1', name: 'Mike' }); }); it('API调用失败时抛出错误', async () => { const testError = new Error('请求超时'); mockApiClient.get.mockRejectedValue(testError); await expect(userService.getUser('1')).rejects.toThrow(testError); }); });
方案2:使用Vitest模块Mock跳过依赖链
通过vi.mock直接Mock整个依赖类,无需关心其内部依赖,仅实现被测代码需要的方法。
代码示例
// tests/UserService.test.ts import { describe, beforeEach, it, expect, vi } from 'vitest'; import { UserService } from '../src/services/UserService'; import { ApiClient } from '../src/services/ApiClient'; // Mock整个ApiClient模块,忽略其构造函数依赖 vi.mock('../src/services/ApiClient', () => ({ ApiClient: vi.fn().mockImplementation(() => ({ get: vi.fn() // 仅Mock用到的方法 })) })); describe('UserService', () => { let userService: UserService; let mockApiClient: jest.Mocked<InstanceType<typeof ApiClient>>; beforeEach(() => { // 重置Mock状态 ApiClient.mockClear(); // 获取Mock实例 mockApiClient = new ApiClient() as jest.Mocked<InstanceType<typeof ApiClient>>; userService = new UserService(mockApiClient); }); it('正确调用API获取用户', async () => { mockApiClient.get.mockResolvedValue({ id: '2', name: 'Alice' }); const user = await userService.getUser('2'); expect(mockApiClient.get).toHaveBeenCalledTimes(1); expect(user.name).toBe('Alice'); }); });
确保未定义方法报错的补充技巧
- TypeScript严格类型检查:使用
Pick<IApiClient, 'get'>或自定义接口约束Mock对象,编译阶段就能发现未定义方法的调用。 - Proxy拦截:如方案1中的
createStrictMock函数,运行时直接抛出错误,快速定位测试覆盖遗漏。
更优的代码建模建议
1. 依赖倒置:针对接口编程
将服务的依赖抽象为接口,而非具体类,测试时只需实现接口的必要方法,彻底摆脱对依赖内部结构的依赖:
// 定义接口 export interface IApiClient { get(url: string): Promise<any>; } // 被测服务依赖接口 export class UserService { constructor(private apiClient: IApiClient) {} }
2. 最小化依赖注入
拆分大型服务的职责,每个类仅注入直接需要的依赖,避免出现多层嵌套的依赖链,降低测试Mock的复杂度。
内容的提问来源于stack exchange,提问作者Mike G
相关产品推荐
相关产品推荐

