TypeScript+Jest中如何不设构造参数为可选来Mock依赖类?
在TypeScript+Jest环境下Mock依赖测试RouteController(对标Java Mockito实践)
需求背景
你在使用TypeScript+Jest测试RouteController时,希望像Java中Mockito那样Mock它的依赖RouteService,避免手动创建RouteService实例时需要解析其下游依赖(如UserSession)。RouteController通过tsyringe做依赖注入,构造函数声明必填的RouteService依赖。
可行方案及实现
方案一:手动注入Mock实例(与Java Mockito逻辑最接近)
直接构造RouteController时传入Jest Mock的RouteService实例,完全绕开DI容器,专注测试Controller的业务逻辑。
实现步骤:
- 使用
jest.mock()Mock整个RouteService模块,自动替换模块导出为Mock函数 - 创建Mock实例并定义其方法的行为
- 手动将Mock实例传入
RouteController的构造函数 - 执行测试方法并断言结果
测试代码示例:
import { RouteController } from './RouteController'; import { RouteService } from './RouteService'; import { RouteDTO } from './view/RouteDTO'; // Mock RouteService模块,自动替换所有导出为Jest Mock jest.mock('./RouteService'); describe('RouteController', () => { let controller: RouteController; // 类型断言让TypeScript识别Mock对象的方法(如mockReturnValue) let mockRouteService: jest.Mocked<RouteService>; beforeEach(() => { // 重置Mock的调用记录和返回值,避免测试间污染 jest.clearAllMocks(); // 创建Mock的RouteService实例 mockRouteService = new RouteService() as jest.Mocked<RouteService>; // 手动注入Mock到Controller controller = new RouteController(mockRouteService); }); test('createRoute 业务逻辑有效时,调用saveRoute并返回结果', () => { // 准备测试数据 const testRoute: RouteDTO = { name: 'test-route', path: '/test' }; const mockSavedRoute: RouteDTO = { ...testRoute, id: '123' }; // 定义Mock方法的返回值 mockRouteService.saveRoute.mockReturnValue(mockSavedRoute); // 假设当前测试场景下业务逻辑有效(需根据实际代码调整isBusinessLogicValid的触发条件) const result = controller.createRoute(testRoute); // 断言结果匹配Mock返回值 expect(result).toEqual(mockSavedRoute); // 断言saveRoute被正确调用一次,参数匹配 expect(mockRouteService.saveRoute).toHaveBeenCalledTimes(1); expect(mockRouteService.saveRoute).toHaveBeenCalledWith(testRoute); }); test('createRoute 业务逻辑无效时,抛出错误', () => { // 构造不符合业务逻辑的测试数据 const invalidRoute: RouteDTO = { name: '', path: '/invalid' }; // 执行方法并断言抛出指定错误 expect(() => controller.createRoute(invalidRoute)).toThrow('Invalid business logic'); // 断言saveRoute未被调用 expect(mockRouteService.saveRoute).not.toHaveBeenCalled(); }); });
方案二:结合tsyringe容器替换Mock依赖
如果你习惯通过DI容器获取RouteController实例,可以通过tsyringe的容器API替换RouteService为Mock实例,保持与生产环境一致的实例获取方式。
实现步骤:
- Mock
RouteService模块 - 创建Mock实例并注册到tsyringe容器,替换原有的
RouteService绑定 - 从容器中解析
RouteController实例 - 执行测试并断言
测试代码示例:
import { container } from 'tsyringe'; import { RouteController } from './RouteController'; import { RouteService } from './RouteService'; import { RouteDTO } from './view/RouteDTO'; jest.mock('./RouteService'); describe('RouteController (with tsyringe container)', () => { let controller: RouteController; let mockRouteService: jest.Mocked<RouteService>; beforeEach(() => { jest.clearAllMocks(); mockRouteService = new RouteService() as jest.Mocked<RouteService>; // 注册Mock实例到容器,覆盖原有的RouteService绑定 container.registerInstance(RouteService, mockRouteService); // 从容器中解析Controller实例 controller = container.resolve(RouteController); }); afterEach(() => { // 清空容器实例,避免测试间污染 container.clearInstances(); }); test('createRoute 调用Mock的saveRoute并返回结果', () => { const testRoute: RouteDTO = { name: 'container-test', path: '/container' }; const mockResult: RouteDTO = { ...testRoute, id: '456' }; mockRouteService.saveRoute.mockReturnValue(mockResult); const result = controller.createRoute(testRoute); expect(result).toEqual(mockResult); expect(mockRouteService.saveRoute).toHaveBeenCalledWith(testRoute); }); });
关键注意事项
- 类型支持:使用
jest.Mocked<RouteService>类型断言,让TypeScript识别Mock对象的Jest专属方法(如mockReturnValue、toHaveBeenCalled),避免类型报错。 - 业务逻辑可测试性:原代码中的
isBusinessLogicValid如果是硬编码值,需调整为基于输入参数的判断逻辑,否则无法覆盖两个分支的测试。 - 测试隔离:每次测试前调用
jest.clearAllMocks()重置Mock状态,结合container.clearInstances()(方案二)确保测试间无状态污染。
内容的提问来源于stack exchange,提问作者João Pedro Schmitt
相关产品推荐
相关产品推荐

