Reputation: 61
Having this:
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];
}
);
...
}
}
Goal:
Angular test that covers the ".then(...)" code path.
"loadRemoteModule" should return a value like "{dummy : 123}" given in the Angular test.
Notice:
hundreds of of other tests are working fine.
So I guess the test setup is ok ;-)
Tryed:
(The following list should just provide the basic idea what i´ve already tried .. so please do not nail me when something is missing)
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);
export function loadRemoteModuleWrapper(options: any) { return loadRemoteModule(options); }
let loadRemoteModuleWrapperSpy: jasmine.Spy = spyOn(loadRemoteModuleWrapper as any, 'loadRemoteModuleWrapper');
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,
},
};
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));
beforeEach(() => { return MockBuilder(TaskListComponent) .keep(...) .mock( ...)
})
Any ideas? What do I miss?
UPDATE:
a)
Yes, some of these approaches are pure nonsense - so they show how desperate I am.
b)
All approaches are showing that the function "loadRemoteModule" is called and tryed to be worked out because at test run time I get always an error saying that the requested remote module can not be loaded.
And this error would not occur in case the mock would work.
Update 2:
c)
See
https://github.com/angular-architects/module-federation-plugin/issues/300
update 3:
13)
Object.defineProperty(require('@angular-architects/module-federation'), 'loadRemoteModule', { writable: true });
spyOn(require('@angular-architects/module-federation'), 'loadRemoteModule').and.returnValue(Promise.resolve({ willy: 123 }));
Upvotes: 0
Views: 1294
Reputation: 1062
I found solution of how to unit test loadRemoteModule
First of all I want to say thank you to @manfredsteyer for his very fast response via email with detailed explanation of how it is better to unit test loadRemoteModule. After his message I was able to implement working unit tests. Key idea of this solution is to create wrapper for loadRemoteModule instead of trying to mock it directly.
https://github.com/angular-architects/module-federation-plugin/issues/300#issuecomment-1878415850
module.federation.ts
import { LoadRemoteModuleOptions, loadRemoteModule } from '@angular-architects/module-federation';
export const ModuleFederationWrapper = {
loadRemoteModule<T = any>(options: LoadRemoteModuleOptions): Promise<T>
{
return loadRemoteModule<T>(options);
}
}
app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ModuleFederationWrapper } from '../module.federation';
export const routes: Routes = [
{
path: 'news',
loadChildren: () =>
ModuleFederationWrapper.loadRemoteModule({
type: 'manifest',
remoteName: 'news',
exposedModule: './Module'
})
.then(x => x.NewsModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
app-routing.module.spec.ts
import { Location } from "@angular/common";
import { Router } from "@angular/router";
import { routes } from "./app-routing.module";
import { LoadRemoteModuleOptions } from "@angular-architects/module-federation";
import { AppComponent } from "./app.component";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { HeaderProxyComponent } from "./plugins/header-proxy/header-proxy.component";
import { ModuleFederationWrapper } from "../module.federation";
describe('AppRoutingModule', () => {
describe('routes', () => {
it('length should be one', () => {
// Arrange & Act & Assert
expect(routes.length).toEqual(1);
});
it('news', () => {
// Arrange & Act
let route = routes[0];
// Assert
expect(route.path).toBe('news');
});
});
describe('routes: navigate', () => {
let location: Location;
let router: Router;
let fixture: ComponentFixture<AppComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(routes)],
declarations: [
AppComponent,
HeaderProxyComponent
]
})
.compileComponents();
router = TestBed.inject(Router);
location = TestBed.inject(Location);
fixture = TestBed.createComponent(AppComponent);
router.initialNavigation();
});
it('should load "news" module', async () => {
// Arrange
const loadRemoteModuleOptions: LoadRemoteModuleOptions = {
type: 'manifest',
remoteName: 'news',
exposedModule: './Module'
};
const fakeModule: any = { };
const remoteModule: any = {
NewsModule: fakeModule
};
var mockLoadRemoteModule = spyOn(ModuleFederationWrapper, 'loadRemoteModule');
mockLoadRemoteModule
.withArgs(loadRemoteModuleOptions)
.and.returnValue(Promise.resolve(remoteModule));
// Act
const route = router.config.find(x => x.path === 'news');
// Assert
expect(await route?.loadChildren?.()).toBe(fakeModule);
});
});
});
https://www.angulararchitects.io/en/blog/module-federation-with-angulars-standalone-components/
header-proxy.component.ts
import { Component, ComponentRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { ModuleFederationWrapper } from '../../../module.federation';
@Component({
selector: 'app-header-proxy',
templateUrl: './header-proxy.component.html'
})
export class HeaderProxyComponent implements OnInit {
@Input()
title!: string;
@ViewChild('placeHolder', { read: ViewContainerRef })
viewContainer!: ViewContainerRef;
component!: ComponentRef<unknown>;
ngOnInit(): void {
(async () => {
await this.loadHeader();
})();
}
async loadHeader(): Promise<void> {
const remoteModule = await ModuleFederationWrapper.loadRemoteModule({
type: 'manifest',
remoteName: 'header',
exposedModule: './HeaderComponent'
});
this.component = this.viewContainer.createComponent(remoteModule.HeaderComponent);
this.component.setInput('title', this.title);
}
}
header-proxy.component.spec.ts
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { LoadRemoteModuleOptions } from '@angular-architects/module-federation';
import { ModuleFederationWrapper } from '../../../module.federation';
import { HeaderProxyComponent } from "./header-proxy.component";
import { Component } from "@angular/core";
@Component({
selector: 'app-fake',
template: '<p>Fake Component</p>'
})
class FakeComponent { }
describe('HeaderProxyComponent', () => {
let component: HeaderProxyComponent;
let fixture: ComponentFixture<HeaderProxyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
HeaderProxyComponent
]
})
.compileComponents();
fixture = TestBed.createComponent(HeaderProxyComponent);
component = fixture.componentInstance;
});
it('should create', () => {
// Arrange & Act & Assert
expect(component).toBeTruthy();
});
describe('ngOnInit()', () => {
it('should call "loadHeader()"', () => {
// Arrange
const mockLoadHeader = spyOn(component, 'loadHeader');
// Act
component.ngOnInit();
// Arrange & Act & Assert
expect(mockLoadHeader).toHaveBeenCalledTimes(1);
});
});
describe('loadHeader()', () => {
it('should loadRemoteModule', async () => {
// Arrange
fixture.detectChanges();
const loadRemoteModuleOptions: LoadRemoteModuleOptions = {
type: 'manifest',
remoteName: 'header',
exposedModule: './HeaderComponent'
}
const fakeComponentFixture = TestBed.createComponent(FakeComponent);
const remoteModule: any = {
HeaderComponent: fakeComponentFixture.componentInstance
};
var mockLoadRemoteModule = spyOn(ModuleFederationWrapper, 'loadRemoteModule');
mockLoadRemoteModule
.withArgs(loadRemoteModuleOptions)
.and.returnValue(Promise.resolve(remoteModule));
let mockCreateComponent = spyOn(component.viewContainer, 'createComponent');
mockCreateComponent
.withArgs(remoteModule.HeaderComponent)
.and.returnValue(fakeComponentFixture.componentRef);
let mockSetInput = spyOn(fakeComponentFixture.componentRef, 'setInput');
// Act
await component.loadHeader();
// Assert
expect(mockLoadRemoteModule).toHaveBeenCalledOnceWith(jasmine.objectContaining(loadRemoteModuleOptions));
expect(mockCreateComponent).toHaveBeenCalledOnceWith(jasmine.objectContaining(remoteModule.HeaderComponent));
expect(mockSetInput).toHaveBeenCalledOnceWith(jasmine.stringMatching('title'), component.title);
});
});
});
Upvotes: 0