Angular测试:如何用Jasmine Mock ModuleFederation的loadRemoteModule?
Angular模块联邦loadRemoteModule测试Mock方案问题
现有代码
import { loadRemoteModule } from '@angular-architects/module-federation'; export class TaskListComponent implements OnInit { ... ngOnInit(): void { this.loadRemoteComponent({ componentName: 'testComponentName', exposedName: 'testExposedName', url: 'testUrl', params: { valueA: 'testParamsValueA', valueB: true } }); } private async loadRemoteComponent(config: AngularElementConfig) { const componentType = await loadRemoteModule({ type: 'module', remoteEntry: config.url, exposedModule: config.exposedName }) .then(m => { return m[config.componentName]; }); ... } }
需求
编写Angular测试,覆盖上述代码中的.then(...)代码路径,让loadRemoteModule在测试中返回类似{dummy : 123}的指定值。
现状
其他数百个测试均正常运行,推测测试环境配置无问题。
已尝试方案
- 使用rewire工具
- 使用mockrewire工具
const spy = spyOn(loadRemoteModule as any, 'loadRemoteModule').and.returnValue(Promise.resolve(returnValue));const spy = spyOn(ModuleFederation, 'loadRemoteModule').and.returnValue(Promise.resolve(returnValue));const spy = spyOn(ModuleFederation as any, 'loadRemoteModule').and.returnValue(returnValue);- 封装wrapper函数并spy:
export function loadRemoteModuleWrapper(options: any) { return loadRemoteModule(options); } let loadRemoteModuleWrapperSpy: jasmine.Spy = spyOn(loadRemoteModuleWrapper as any, 'loadRemoteModuleWrapper'); - 创建SpyObj并替换:
const mockModuleFederation = jasmine.createSpyObj('ModuleFederation', ['loadRemoteModule']); beforeEach(() => { (ModuleFederation as any).loadRemoteModule = mockModuleFederation.loadRemoteModule; }); afterEach(() => { delete (ModuleFederation as any).loadRemoteModule; }); mockModuleFederation.loadRemoteModule.and.returnValue(Promise.resolve(returnValue)); - 自定义模块加载器:
const customLoader: any = (module: string) => { if (module === '@angular-architects/module-federation') { return { loadRemoteModule: jasmine.createSpy('loadRemoteModule').and.returnValue(Promise.resolve({ willy: 123 })), }; } else { return (window as any).require(module); } }; const mockLoadRemoteModule = jasmine.createSpy('loadRemoteModule').and.returnValue(Promise.resolve(returnValue)); (window as any).require = { '@angular-architects/module-federation': { loadRemoteModule: mockLoadRemoteModule, }, }; - 用rewire替换方法:
const ModuleFederation = rewire('@angular-architects/module-federation'); ModuleFederation.__set__('loadRemoteModule', jasmine.createSpy('loadRemoteModule').and.returnValue(Promise.resolve(returnValue))); - 直接替换原方法:
const originalLoadRemoteModule = ModuleFederation.loadRemoteModule; ModuleFederation.loadRemoteModule = jasmine.createSpy('loadRemoteModule').and.returnValue(Promise.resolve(returnValue)); - 使用MockBuilder的.keep/.mock/.provide方法:
beforeEach(() => { return MockBuilder(TaskListComponent) .keep(...) .mock( ...) }) - 修改属性可写性后spy:
Object.defineProperty(require('@angular-architects/module-federation'), 'loadRemoteModule', { writable: true }); spyOn(require('@angular-architects/module-federation'), 'loadRemoteModule').and.returnValue(Promise.resolve({ willy: 123 }));
问题
所有尝试方案均未生效,测试时仍报错“无法加载请求的远程模块”,说明mock未成功拦截原方法调用,求可行的mock方案。
更新说明
- 部分尝试方案并不合理,仅为尝试不同思路;
- 报错信息表明原
loadRemoteModule仍被调用; - 已参考Angular Architects模块联邦插件GitHub仓库的相关Issue(编号300)。
可行解决方案
方案1:封装成Angular服务(推荐)
将loadRemoteModule封装为可注入的服务,通过依赖注入的方式在组件中使用,这样测试时可以轻松mock服务方法。
步骤1:创建远程加载服务
// remote-loader.service.ts import { Injectable } from '@angular/core'; import { loadRemoteModule, LoadRemoteModuleOptions } from '@angular-architects/module-federation'; @Injectable({ providedIn: 'root' }) export class RemoteLoaderService { loadRemoteModule(options: LoadRemoteModuleOptions) { return loadRemoteModule(options); } }
步骤2:修改组件,注入服务
// task-list.component.ts import { RemoteLoaderService } from './remote-loader.service'; export class TaskListComponent implements OnInit { constructor(private remoteLoaderService: RemoteLoaderService) {} // ... private async loadRemoteComponent(config: AngularElementConfig) { const componentType = await this.remoteLoaderService.loadRemoteModule({ type: 'module', remoteEntry: config.url, exposedModule: config.exposedName }) .then(m => m[config.componentName]); // ... } }
步骤3:测试中mock服务
// task-list.component.spec.ts import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TaskListComponent } from './task-list.component'; import { RemoteLoaderService } from './remote-loader.service'; describe('TaskListComponent', () => { let component: TaskListComponent; let fixture: ComponentFixture<TaskListComponent>; let mockRemoteLoader: jasmine.SpyObj<RemoteLoaderService>; beforeEach(async () => { // 创建spy对象 mockRemoteLoader = jasmine.createSpyObj('RemoteLoaderService', ['loadRemoteModule']); await TestBed.configureTestingModule({ declarations: [TaskListComponent], providers: [ { provide: RemoteLoaderService, useValue: mockRemoteLoader } ] }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(TaskListComponent); component = fixture.componentInstance; }); it('should cover the .then() path with mock return value', async () => { // 模拟返回值,匹配组件中需要的componentName const mockModule = { testComponentName: {} }; mockRemoteLoader.loadRemoteModule.and.returnValue(Promise.resolve(mockModule)); // 调用目标方法 await component.loadRemoteComponent({ componentName: 'testComponentName', exposedName: 'testExposedName', url: 'testUrl', params: { valueA: 'testParamsValueA', valueB: true } }); // 验证调用和返回值处理 expect(mockRemoteLoader.loadRemoteModule).toHaveBeenCalledWith({ type: 'module', remoteEntry: 'testUrl', exposedModule: 'testExposedName' }); // 可根据实际逻辑添加对componentType的断言 }); });
方案2:直接mock模块导出函数(无需修改组件)
利用ES模块的属性可配置性,在测试中修改@angular-architects/module-federation的导出函数:
// task-list.component.spec.ts import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TaskListComponent } from './task-list.component'; import * as ModuleFederation from '@angular-architects/module-federation'; describe('TaskListComponent', () => { let component: TaskListComponent; let fixture: ComponentFixture<TaskListComponent>; let loadRemoteModuleSpy: jasmine.Spy; beforeEach(async () => { // 修改导出函数为可写,并创建spy Object.defineProperty(ModuleFederation, 'loadRemoteModule', { writable: true, value: jasmine.createSpy('loadRemoteModule') }); loadRemoteModuleSpy = ModuleFederation.loadRemoteModule; await TestBed.configureTestingModule({ declarations: [TaskListComponent] }).compileComponents(); }); afterEach(() => { // 清理修改,避免影响其他测试 delete ModuleFederation.loadRemoteModule; }); it('should cover the .then() path with mock return value', async () => { const mockModule = { testComponentName: 'mockComponentInstance' }; loadRemoteModuleSpy.and.returnValue(Promise.resolve(mockModule)); await component.loadRemoteComponent({ componentName: 'testComponentName', exposedName: 'testExposedName', url: 'testUrl', params: { valueA: 'testParamsValueA', valueB: true } }); expect(loadRemoteModuleSpy).toHaveBeenCalled(); // 验证then中的逻辑是否正确执行 }); });
内容的提问来源于Stack Exchange,提问作者displayname
相关产品推荐
相关产品推荐

