Testing routed components with RouterTestingHarness

Cover art by Microsoft Designer.

Angular's official documentation has, since 2017, offered scant guidance on testing routing components, routed components, routes, and route guards. The standard advice has been to construct partial and brittle test doubles for ActivatedRoute and Router.

Although RouterTestingModule—now interchangeable with the standalone APIs provideRouter and provideLocationMocks—has existed since Angular version 2, documentation has been sparse at best.

In February 2023, Angular version 15.2 shipped RouterTestingHarness, providing an official solution to this six-year-old testing challenge. The RouterTestingHarness creates a test root component containing a RouterOutlet and leverages the genuine Angular Router API inside component tests.

Tour of Heroes router tutorial: DashboardComponent

The examples in this piece draw from Angular.io's Tour of Heroes router tutorial.

To employ the RouterTestingHarness, begin by configuring a test for a routing component or a routed component with provideRouter, exactly as you would wire up any standalone Angular application or feature.

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

One approach is to supply only the minimal test routes needed for the component under test, as the listing above shows. Alternatively, you could include—and thereby exercise—the actual feature routes. Using the real routes within component tests forms a key part of what is known as Angular feature tests.

💡 Tip
provideLocationMocks replaces the Location and LocationStrategy services consumed internally by the Angular Router with test doubles. This isolation keeps tests independent from browser History and Location APIs, which could otherwise trigger navigation in test runners operating within a real browser—such as Karma, Web Test Runner, or Cypress Component Test Runner—and which may be entirely absent in browser-less runners like Jest or Mocha.

After setting up the test dependencies, invoke and await the static method RouterTestingHarness.create to obtain an instance of the RouterTestingHarness, as illustrated below.

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

An initial URL could have been provided to the method to directly activate the component under test, HeroDetailComponent. Instead, this will be handled via the RouterTestingHarness#navigateByUrl method.

⚠️ Warning
Calling RouterTestingHarness.create is restricted to a single invocation per test case. This also requires ModuleTeardownOptions#destroyAfterEach to be set to true, which is the default.

The RouterTestingHarness#navigateByUrl method takes an optional component argument. When supplied, it verifies that the component activated after navigating to the given URL matches this expectation, as shown in the next snippet.

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

As the code above indicates, the Promise resolved by the RouterTestingHarness#navigateByUrl method yields the instance of the activated component.

For test assertions, it is advisable to interact with the DOM. This approach exercises the component template as well, and sidesteps reliance on internal implementation details—such as whether the loaded hero is stored as a plain object, an RxJS Observable, or an Angular Signal. The result is a less fragile test that accommodates refactoring of the component and its collaborators without necessitating test updates.

Tour of Heroes router tutorial: HeroDetailComponent

DOM-based assertions can be made through either the RouterTestingHarness#routeDebugElement or the RouterTestingHarness#routeNativeElement property.

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

The example above uses the RouterTestingHarness#routeNativeElement property to access the DOM managed by Angular and HeroDetailComponent. The hero heading is located, and its content is verified to be Dr. Nice, matching the hero associated with ID 12 from the application URL used in the RouterTestingHarness#navigateByUrl call.

Here is the complete test suite:

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

A routed Angular component has been successfully exercised within a component test. Moreover, thanks to the use of RouterTestingHarness and DOM-centric assertions, the test remains robust against refactorings of both the component and its services.

As a practical challenge, create a routed component test similar to the one above. Next, refactor the component to receive its data through an input property instead of depending on ActivatedRoute, while enabling the withComponentInputBinding Angular Router feature. The sole modification required in your test will be adding the same Angular Router feature to the test module configuration.