Cover art by DALL·E 2.
It's been three years since Testing Angular routing components with the RouterTestingModule. This article revisits integrated routing component tests with modern Angular APIs, including standalone components, provideRouter, provideLocationMocks, and RouterTestingHarness. Additionally, we use a SIFERS for managing our test setup and test utilities.
providerRouter and provideLocationMocks
provideRouter (introduced by Angular version 14.2) is the standalone version of RouterModule.forRoot. Combine it with provideLocationMocks (introduced by Angular version 15.0) and we have the standalone version of RouterTestingModule.withRoutes.
ℹ️ Note
Read What does the RouterTestingModule do? for a detailed explanation of howRouterTestingModulereplaces Angular Router dependencies.provideLocationMocksdoes the same.
RouterTestingHarness
RouterTestingHarness (introduced by Angular version 15.2) is similar to Spectacular's Feature testing API.
When we call RouterTestingHarness.create (only call it once per test), a test root component with a router outlet is created behind the scenes but we don't get access to this component or its component fixture.
The resolved RouterTestingHarness instance has the properties routeDebugElement and routeNativeElement which access the DebugElement and HTMLElement corresponding to the component currently activated by the test root component's RouterOutlet.
RouterTestingHarness has a detectChanges method which calls ComponentFixture#detectChanges for the test root component.
The RouterTestingHarness#navigateByUrl method wraps Router#navigateByUrl and resolves the component activated by that navigation.
That's all the background we need. Let's explore a RouterTestingHarness version of the integrated routed component test for the DashboardComponent from the Tour of Heroes Router tutorial.
Integrated routing component test suite
import { Location } from '@angular/common';
import { provideLocationMocks } from '@angular/common/testing';
import { Component } from '@angular/core';
import {
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { asapScheduler, of } from 'rxjs';
import { observeOn } from 'rxjs/operators';
import { HeroService } from '../hero.service';
import { HEROES } from '../mock-heroes';
import { DashboardComponent } from './dashboard.component';
async function setup() {
const fakeService = {
getHeroes() {
return of([...HEROES]).pipe(observeOn(asapScheduler));
},
} as Partial<HeroService>;
TestBed.configureTestingModule({
providers: [
provideRouter([
{
path: '',
pathMatch: 'full',
component: DashboardComponent,
},
{
path: 'detail/:id',
component: TestHeroDetailComponent,
},
]),
provideLocationMocks(),
{ provide: HeroService, useValue: fakeService },
],
});
const harness = await RouterTestingHarness.create(); // [1]
const location = TestBed.inject(Location);
return {
advance() {
tick();
harness.detectChanges();
},
clickTopHero() {
const firstHeroLink = harness.routeDebugElement.query(
By.css('a')
);
firstHeroLink.triggerEventHandler('click', {
button: leftMouseButton,
});
},
harness,
location,
};
}
@Component({
standalone: true,
template: '',
})
class TestHeroDetailComponent {}
const leftMouseButton = 0;
describe('DashboardComponent (integrated)', () => {
it('navigates to the detail view when a hero link is clicked', fakeAsync(async () => {
const { advance, clickTopHero, harness, location } =
await setup();
const component /* [2] */ = await harness.navigateByUrl(
'/',
DashboardComponent // [3]
);
const [topHero] = component.heroes;
clickTopHero();
advance();
const expectedPath = '/detail/' + topHero.id;
expect(location.path())
.withContext(
'must navigate to the detail view for the top hero'
)
.toBe(expectedPath);
}));
});
(1) Notice how we only call RouterTestingHarness.create once per test case in our setup SIFERS.
⚠️ Warning
ModuleTeardownOptions#destroyAfterEachmust be set totrueforRouterTestingHarnessto work correctly. See Improving Angular tests by enabling Angular testing module teardown for details on this option.
(1) We could have passed an initial URL, for example await RouterTestingHarness.create("/") or await RouterTestingHarness.create("/heroes") but it doesn't return an activated component.
(2) RouterTestingHarness#navigateByUrl resolves an activated component and optionally accepts the type (class) of the activated component we expect (3). If the component activated by that navigation is not of the expected type, an error is thrown.
The full test suite is available in this Gist.
Summary
Let's sum up what we learned in this article:
-
RouterTestingHarness(introduced by Angular version 15.2) is a testing harness specifically for interacting with Angular Router-related APIs in tests -
provideRouter(introduced by Angular version 14.2) is the standalone version ofRouterModule.forRoot. -
provideLocationMocks(introduced by Angular version 15.0) is the standalone version ofRouterTestingModule - The standalone version of
RouterTestingModule.withRoutesisprovideRouterandprovideLocationMockscombined.
RouterTestingHarness.create creates an initializes a test root component with a router outlet. It must only be called once per test case and requires ModuleTeardownOptions#destroyAfterEach to be set to true. It optionally accepts an initial URL.
RouterTestingHarness#navigateByUrl accepts a URL for navigation and optionally the expected type of the component activated by that navigation. The activated component is resolved by the method call.
RouterTestingHarness#detectChanges triggers a change detection cycle starting at the test root component.

Top comments (4)
Thanks for nice and tidy update!)
I cannot get where should we use this
ModuleTeardownOptions#destroyAfterEach.It seems you have nothing concerned with
destroyAfterEachin you code example, have you?I'm glad you found it useful, Arthur. Have a look at dev.to/this-is-angular/improving-a..., also linked in this article.
Nice article. I'm curious to know why you prefer a custom
setup()function over Jest's standardbeforeEach()Thank you, Yoann. This is the SIFERS pattern medium.com/@kolodny/testing-with-s...