displayname
displayname

Reputation: 61

ModuleFederation, Angular test, jasmine: How to mock "loadRemoteModule"?

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)

  1. rewire

  2. mockrewire

  3. const spy = spyOn(loadRemoteModule as any, 'loadRemoteModule').and.returnValue(Promise.resolve(returnValue));

  4. const spy = spyOn(ModuleFederation, 'loadRemoteModule').and.returnValue(Promise.resolve(returnValue));

  5. const spy = spyOn(ModuleFederation as any, 'loadRemoteModule').and.returnValue(returnValue);

  6. 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));
  1. A lot of .keep, .mock, and .provide´s

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

Answers (1)

Vladimir Rodchenko
Vladimir Rodchenko

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

Create wrapper for loadRemoteModule

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);
  }
}

Option 1

Using loadRemoteModule wrapper for Routes

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 { }

Unit tests

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);
    });
  });
});

Option 2

Using loadRemoteModule wrapper when programmatic component loading

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);
  }

}

Unit tests

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

Related Questions