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 howRouterTestingModule
replaces Angular Router dependencies.provideLocationMocks
does 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#destroyAfterEach
must be set totrue
forRouterTestingHarness
to 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.withRoutes
isprovideRouter
andprovideLocationMocks
combined.
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
destroyAfterEach
in 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...