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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 09:57:21