Cover art by DALL·E 2.
Three years have passed since we last explored testing Angular routing components with RouterTestingModule. Now we're revisiting integrated routing component tests using modern Angular features—standalone components, provideRouter, provideLocationMocks, and RouterTestingHarness. Our test setup and utilities follow the SIFERS approach.
providerRouter and provideLocationMocks
Angular 14.2 brought us provideRouter, which serves as the standalone counterpart to RouterModule.forRoot. When paired with provideLocationMocks—introduced in Angular 15.0—we get the standalone equivalent of RouterTestingModule.withRoutes.
ℹ️ Note
For a thorough breakdown of howRouterTestingModuleswaps out Angular Router dependencies, check out the explanation in What does the RouterTestingModule do?. The behavior ofprovideLocationMocksmirrors that process.
RouterTestingHarness
RouterTestingHarness, which arrived with Angular 15.2, shares similarities with the Feature testing API from Spectacular.
Behind the scenes, calling RouterTestingHarness.create (remember: once per test case only) sets up a test root component containing a router outlet. However, that component and its fixture remain hidden from us.
The resulting RouterTestingHarness object exposes routeDebugElement and routeNativeElement properties. These give us access to the DebugElement and HTMLElement of whatever component the test root's RouterOutlet currently has active.
There's also a detectChanges method on RouterTestingHarness that triggers ComponentFixture#detectChanges for the test root component.
Navigation is handled by RouterTestingHarness#navigateByUrl, which wraps Router#navigateByUrl and returns the component activated by the navigation.
With that context in mind, let's look at how the DashboardComponent test from the Tour of Heroes Router tutorial can be rewritten as an integrated routed component test using RouterTestingHarness.
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) Inside our setup SIFERS, we invoke RouterTestingHarness.create just once per test.
⚠️ Warning
ForRouterTestingHarnessto function properly,ModuleTeardownOptions#destroyAfterEachneeds to betrue. More information about this setting can be found in Improving Angular tests by enabling Angular testing module teardown.
(1) An initial URL could have been supplied—something like await RouterTestingHarness.create("/") or await RouterTestingHarness.create("/heroes")—though the activated component wouldn't be returned in that case.
(2) RouterTestingHarness#navigateByUrl gives us back the component that got activated. It also takes an optional second argument: the expected type (class) of that component (3). Should the actual component not match the expected type, an error gets thrown.
You can find the complete test suite in this Gist.
Summary
Here's a recap of what we've covered:
-
RouterTestingHarness(introduced by Angular version 15.2) is a testing harness built specifically for working with Angular Router APIs in test scenarios. -
provideRouter(introduced by Angular version 14.2) serves as the standalone equivalent ofRouterModule.forRoot. -
provideLocationMocks(introduced by Angular version 15.0) is the standalone version ofRouterTestingModule. - Together,
provideRouterandprovideLocationMocksform the standalone replacement forRouterTestingModule.withRoutes.
When you call RouterTestingHarness.create, it sets up your test root component with a router outlet. This method should be invoked exactly once per test, requires ModuleTeardownOptions#destroyAfterEach equal to true, and can optionally take an initial URL.
With RouterTestingHarness#navigateByUrl, you pass in the URL for navigation and, optionally, the expected component type. The method resolves to whichever component gets activated by that navigation.
Calling RouterTestingHarness#detectChanges runs change detection starting from the test root component.

