DEV Community

Cover image for Testing routed components with RouterTestingHarness
Lars Gyrup Brink Nielsen for This is Angular

Posted on • Updated on

Testing routed components with RouterTestingHarness

Cover art by Microsoft Designer.

Since 2017, Angular documentation has offered little advice on testing routing components, routed components, routes, and route guards other than to create partial and brittle ActivatedRoute and Router test doubles.

While RouterTestingModule—recently replacable using the equivalent standalone APIs, provideRouter and provideLocationMocks—has been available since Angular version 2, documentation has been lacking at best.

In February 2023, Angular version 15.2 introduced RouterTestingHarness, the out-of-the-box experience for solving this 6 year old testing pain. The RouterTestingHarness sets up a testing root component with a RouterOutlet and uses the actual Angular Router API in our component tests.

Tour of Heroes router tutorial: DashboardComponent

In this article, we test and use parts of Angular.io's Tour of Heroes router tutorial.

To use the RouterTestingHarness, we first set up a test for a routing component or routed component using provideRouter as we would set up any standalone Angular application or feature.

TestBed.configureTestingModule({
  providers: [
    provideRouter([
      {
        path: 'superhero/:id',
        component: HeroDetailComponent,
      },
    ]),
    provideLocationMocks(),
  ],
});
Enter fullscreen mode Exit fullscreen mode

We could provide just enough test routes for our component test as seen in the previous code listing or even use—and thereby test—our real feature routes. Exercising the actual routes as part of our component tests is one part of what I call Angular feature tests.

💡 Tip
We use provideLocationMocks to replace the Location and LocationStrategy services used internally by the Angular Router with test doubles to isolate the tests from browser History and Location APIs that would trigger navigation in test runners with a real browser environment like Karma, Web Test Runner, or Cypress Component Test Runner and might not be available in test runners without a browser environment like Jest or Mocha.

With dependendencies set up for our test, we call and resolve the static method RouterTestingHarness.create to create an instance of a RouterTestingHarness as demonstrated in the following code snippet.

const harness = await RouterTestingHarness.create();
Enter fullscreen mode Exit fullscreen mode

We could have pass an initial URL to the method to activate the component-under-test, HeroDetailComponent but we will do that using the RouterTestingHarness#navigateByUrl method instead.

⚠️ Warning
RouterTestingHarness.create must only be called once per test case and requires ModuleTeardownOptions#destroyAfterEach to be set to true (the default value).

The RouterTestingHarness#navigateByUrl method optionally accepts the component we want to activate and asserts that this is the result of navigating to the specified application URL as seen in the following code snippet.

const component = await harness
  .navigateByUrl('superhero/12', HeroDetailComponent);
Enter fullscreen mode Exit fullscreen mode

As suggested by the previous code snippet, the activated component instance is resolved by the Promise returned by the RouterTestingHarness#navigateByUrl method.

I recommend that we make test assertions via the DOM to also exercise the component template. Additionally, this avoid relying on implementation details like whether the loaded hero is represented as a raw data structure, an RxJS Observable, or an Angular Signal. This decreases the brittleness of the test to support refactoring the component and its collaborators without breaking our component test.

Tour of Heroes router tutorial: HeroDetailComponent

To make assertions via the DOM, we use either of the RouterTestingHarness#routeDebugElement or the RouterTestingHarness#routeNativeElement properties.

const heading = harness
  .routeNativeElement
  ?.querySelector('h3')
  ?.textContent
  ?.trim() ?? '';
expect(heading).toBe('Dr. Nice');
Enter fullscreen mode Exit fullscreen mode

In the previous code snippet, we use the RouterTestingHarness#routeNativeElement property to access the DOM managed by Angular and HeroDetailComponent. We query for the hero heading and assert its content to be Dr. Nice, the name of the hero who has the ID of 12 as specified in the application URL we navigated to using the RouterTestingHarness#navigateByUrl method.

The final test suite looks something like this:

import { provideLocationMocks } from '@angular/common/testing';
import { TestBed } from '@angular/core/testing';
import {
  provideRouter,
  withComponentInputBinding,
} from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { HeroDetailComponent } from './hero-detail.component';

describe(HeroDetailComponent.name, () => {
  it('displays the name of the hero', async () => {
    TestBed.configureTestingModule({
      providers: [
        provideRouter([
          {
            path: 'superhero/:id',
            component: HeroDetailComponent,
          },
        ]),
        provideLocationMocks(),
      ],
    });

    const harness = await RouterTestingHarness.create();
    const component = await harness.navigateByUrl(
      'superhero/12',
      HeroDetailComponent
    );

    const heading = harness
      .routeNativeElement
      ?.querySelector('h3')
      ?.textContent
      ?.trim() ?? '';
    expect(heading).toBe('Dr. Nice');
  });
});
Enter fullscreen mode Exit fullscreen mode

Great, we successfully exercised a routed Angular component in a component test! Better yet, because the test uses RouterTestingHarness and makes assertions via the DOM, it is resilient to component and service refactorings.

As an exercise, write a routed component test similar to the one in the previous code listing. Then refactor the component to accept an input property instead of depending on ActivatedRoute and provide the withComponentInputBinding Angular Router feature. The only thing you should have to change in your test is to provide the same Angular Router feature to the Angular tesitng module.

Top comments (2)

Collapse
 
jangelodev profile image
João Angelo

TOP Lars Gyrup,
Obrigado por compartilhar

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

Thank you, João