getsaf / shallow-render

Angular testing made easy with shallow rendering and easy mocking. https://getsaf.github.io/shallow-render
MIT License
273 stars 25 forks source link

Problem with mocking of RouterModule (Angular 14 + Jest) #226

Closed mimajka closed 1 year ago

mimajka commented 2 years ago

In the process of updating my project to angular 14, in which I am using Jest and Shallow-render for unit tests, I noticed a problem with mocking of RouterModule. Unit tests failed with the following error message.

  AppComponent
    ✕ should match snapshot (11 ms)

  ● AppComponent › should match snapshot

    MockOfRouterOutlet does not have a module def (ɵmod property)

      11 |
      12 |   it('should match snapshot', async () => {
    > 13 |     const { fixture } = await shallow.render();
         |                                       ^
      14 |
      15 |     expect(fixture).toMatchSnapshot();
      16 |   });

      at transitiveScopesFor (node_modules/@angular/core/fesm2020/core.mjs:24497:11)
          at Array.forEach (<anonymous>)
          at Array.forEach (<anonymous>)
          at Array.forEach (<anonymous>)
      at src/app/app.component.spec.ts:13:39
      at src/app/app.component.spec.ts:12:42

  ● AppComponent › should match snapshot

    MockOfRouterOutlet does not have a module def (ɵmod property)

      at transitiveScopesFor (node_modules/@angular/core/fesm2020/core.mjs:24497:11)
          at Array.forEach (<anonymous>)
          at Array.forEach (<anonymous>)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.101 s, estimated 13 s

Minimal reproduction of the problem

You can reproduce the issue by running ng test for the project in my repo where you find a simple Angular 14 project with Jest 28 (added with @angular-builders/jest) and Shallow-render 14.

npm i
ng test
mfrey-WELL commented 2 years ago

I had the same issue and saw in the example test that they used RouterTestingModule instead of RouterModule. Adding a global replacement fixed it for me: Shallow.alwaysReplaceModule(RouterModule, RouterTestingModule);

mimajka commented 2 years ago

Thank you @mfrey-WELL for your comment. I know about using RouterTestingModule instead of RouterModule, however it dosn't work for all tests in my project. For example when I subscribe to Router events in a component and want to mock these events in a test, it's not doable with RouterTestingModule or at least I have no idea how I could do it 😉

floisloading commented 2 years ago

Is your error gone with the line alwaysReplaceModule(RouterModule, RouterTestingModule) @Mimajka ?

For testing Router events, you could inject a custom Mock - something like:

  it('should do something on NavigationEnd event', async () => {
    const mockEvent = new NavigationEnd(...);
    const mockRouter = { events: of(mockEvent) };
    const { inject } = await shallow.provide({ provide: Router, useValue: mockRouter }).render();
    ....
  });
});
johnwest80 commented 1 year ago

@getsaf Brandon, did you see this one? we're hitting up against it as we migrate to Angular 14. could you take a look? thx!

getsaf commented 1 year ago

I've never tried to write any specs against the non-test RouterModule. I would guess that things wouldn't go well w/out manually providing some suitable mocks for it (or using the RouterTestingModule as @mfrey-WELL suggested).

You may want to try avoiding mocking the actual RouterModule and RouterOutlet with

shallow.dontMock(RouterModule, RouterOutlet)

If for some reason this doesn't work and you can't use Angular's RouterTestingModule, @floisloading's solution sounds good too.

If neither of those solutions work, you may need to try a TestBed-only spec.

getsaf commented 1 year ago

fast-follow: I did just try modifying the routing tests in the shallow-render repo and it worked:

describe('component with routing', () => {
  let shallow: Shallow<GoHomeLinkComponent>;

  beforeEach(() => {
    shallow = new Shallow(GoHomeLinkComponent, GoHomeModule).dontMock(
      RouterModule,
      RouterOutlet,
    );
  });

  it('uses the route', async () => {
    const { fixture, find, inject } = await shallow.render();
    const location = inject(Location);
    find('a').triggerEventHandler('click', {});
    await fixture.whenStable();

    expect(location.path()).toMatch(/\/home$/);
  });
});
jebner commented 1 year ago

For us it didn't work to provide a mock for Router using shallow.provide({ provide: Router, useValue: mockRouter }) as suggested by @floisloading. Because when adding dontMock(RouterModule) e.g. the RouterLinkWithHref directive seemed to need some of the other methods on Router (TypeError: Cannot read properties of undefined (reading 'subscribe') at new RouterLinkWithHref). If we don't add dontMock(RouterModule) we get errors like described above: MockOfRouterOutlet does not have a module def (ɵmod property). Similar problems for using replaceModule(RouterModule, RouterTestingModule).

But we could fix the problem by only mocking a single method on the Router like this:

        const rendering = await shallow
            // [...] mocks etc. for router unrelated things
            .dontMock(RouterModule, ActivatedRoute)
           .provide({provide: ActivatedRoute, useValue: {
                data: activatedRouteData.asObservable(),
                queryParams: activatedRouteQueryParams.asObservable()
            }})
            .render();

        router = rendering.inject(Router);

        spyOn(router, "navigate").and.callFake((commands: any[], extras?: NavigationExtras) => {
            activatedRouteQueryParams.next(extras?.queryParams || {});
            return Promise.resolve(true);
        });
getsaf commented 1 year ago

This looks solved :-)