In Angular, what does RouterTestingHarness give a routed-component test, and how do its navigateByUrl and routeNativeElement behave?
answer
- the real router inside TestBed
- a root fixture with an outlet
- await a navigation, get the instance
- a typed overload that throws
basics
~10 sRouterTestingHarness runs the real router in a unit test: with provideRouter(routes) configured, await navigateByUrl(url) completes the navigation, runs change detection and returns the activated component, while routeNativeElement exposes that component's element.
solid answer
~30 s`RouterTestingHarness` from `@angular/router/testing` lets a unit test drive the real router instead of stubbing `ActivatedRoute`. I configure TestBed with `provideRouter(routes)`, call `await RouterTestingHarness.create()`, which builds a root fixture hosting a `RouterOutlet`, and then `await harness.navigateByUrl('/users/42', UserProfilePage)`. That waits for the navigation, redirects included, runs change detection, and returns the routed component instance typed as `UserProfilePage`; if a guard sent the router elsewhere, the typed overload throws. `routeNativeElement` and `routeDebugElement` give the routed component's element, or `null` when nothing was activated. It replaces the deprecated `RouterTestingModule`, and guards and resolvers run for real.
code
ts · 26 linesimport { Component, inject } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ActivatedRoute, provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
@Component({
selector: 'app-user-profile-page',
template: '<h1>Profile {{ userId }}</h1>',
})
class UserProfilePage {
readonly userId = inject(ActivatedRoute).snapshot.paramMap.get('id');
}
describe('UserProfilePage routing', () => {
it('renders the profile for the id in the URL', async () => {
TestBed.configureTestingModule({
providers: [provideRouter([{ path: 'users/:id', component: UserProfilePage }])],
});
const harness = await RouterTestingHarness.create();
const page = await harness.navigateByUrl('/users/42', UserProfilePage);
expect(page.userId).toBe('42');
expect(harness.routeNativeElement?.textContent).toContain('Profile 42');
});
});go deeper
Recall the setup: provideRouter with the routes, RouterTestingHarness.create(), await navigateByUrl, then assert on routeNativeElement.
Explain what the harness does on each navigation: waits for redirects, runs change detection once, and returns or type-checks the activated component.
Show when a real-router test beats an ActivatedRoute stub, how to combine it with HTTP doubles for data-loading pages, and how to keep those tests from turning into slow integration suites.
Discuss the balance between router-level unit tests and browser end-to-end tests for navigation flows, and which routing behaviours each layer should own.
## The problem it solves A **routed component** reads its data from the router: route parameters through `ActivatedRoute`, resolved data, query parameters, and it only appears after guards allow the navigation. Testing it by creating it directly with `TestBed.createComponent()` means **stubbing `ActivatedRoute`** by hand, and the stub tends to drift from how the real router fills it in. It also skips the guards and resolvers that decide whether the component appears at all. **`RouterTestingHarness`**, from `@angular/router/testing`, runs the **real router** in a unit test. You give TestBed the real route table with `provideRouter(routes)`, navigate to a URL, and the harness renders whatever the router activates, with genuine `ActivatedRoute` values. ## How it works 1. Configure TestBed with `provideRouter([...])` and any doubles the routes need, such as a fake profile API. 2. Call `await RouterTestingHarness.create()`. It creates a root fixture whose template holds a `RouterOutlet` and runs change detection on it. Passing an initial URL, `create('/users/42')`, also navigates there. 3. Call `await harness.navigateByUrl('/users/42')`. It calls `Router.navigateByUrl`, **waits for the navigation to finish, including any redirects**, then runs `detectChanges()` on the root fixture. 4. Assert on what was rendered, through `harness.routeNativeElement` or `harness.routeDebugElement`. The root component is **reused** across navigations in the same test, so a second `navigateByUrl` tests moving from one route to another. ## The members worth knowing | Member | What it gives you | |---|---| | `RouterTestingHarness.create(initialUrl?)` | a harness with a root fixture hosting a `RouterOutlet` | | `navigateByUrl(url)` | the activated component instance, or `null` if the outlet was not activated | | `navigateByUrl(url, ComponentType)` | the instance typed as that component; **throws** if a different component, or none, was activated | | `routeNativeElement` / `routeDebugElement` | the routed component's element, or `null` | | `detectChanges()` | runs change detection on the root fixture | | `fixture` | the underlying `ComponentFixture` for anything else | The typed overload is the one to prefer. `await harness.navigateByUrl('/users/42', UserProfilePage)` both returns a typed instance and fails loudly when a guard redirected somewhere else, instead of letting the test assert on the wrong page. ## What it replaces - **`RouterTestingModule`** is **deprecated**. Its deprecation note says to use `provideRouter` (or `RouterModule.forRoot`) instead. The location fakes it used to add are generally no longer needed, because TestBed provides a mock platform location by default; `provideLocationMocks()` from `@angular/common/testing` adds them explicitly when a test wants them. - **Hand-built `ActivatedRoute` stubs** are still valid for a narrowly isolated component test, but they test your stub's idea of the router. The harness tests the real one. ## Harness or stub: choosing per test | Question the test asks | `ActivatedRoute` stub | `RouterTestingHarness` | |---|---|---| | Does the page render the id it is given? | enough, and fastest | works too | | Does `/users/42` reach this page with id `42`? | cannot answer | the natural fit | | Does a guard or redirect send the user elsewhere? | cannot answer | the typed overload makes it one line | | Does a resolver's data reach the template? | only if you fake the resolved data | runs the real resolver | | Does moving between two routes keep state correct? | awkward | reuse the root fixture across navigations | The practical rule: a stub checks how a component **uses** route data, the harness checks that the **route table** delivers it. A profile page usually deserves one harness test for the route wiring and ordinary component tests for everything else. ## What to keep in mind - `navigateByUrl` runs change detection once. If the routed component loads data asynchronously after it appears, for example a profile GET answered through `HttpTestingController`, run `harness.detectChanges()` or wait for stability after answering it. - A navigation a guard rejects without redirecting leaves the outlet empty, so the untyped overload returns `null` and `routeNativeElement` is `null`. - The harness runs guards and resolvers for real, so their dependencies need doubles too. Testing the guard's own logic in isolation is a separate topic. - It is a unit-test tool, not a browser test: there is no real address bar, and nothing is served over HTTP.
- A guard redirects unauthenticated users to /login. What do the two navigateByUrl overloads return?The untyped `navigateByUrl('/users/42')` waits for the redirect and returns whatever the outlet activated, here the login page instance. The typed `navigateByUrl('/users/42', UserProfilePage)` throws, because the activated component is not a `UserProfilePage`; passing the login component as the type instead turns the redirect itself into the assertion.
- The routed page loads the profile over HttpClient after it appears. What does the test do after navigateByUrl?`navigateByUrl` ran change detection once, when the page appeared. The test then claims the GET with `HttpTestingController.expectOne`, flushes it, and runs `harness.detectChanges()`, or waits for stability, so the template reflects the loaded data before asserting on `routeNativeElement`.
saying these in an interview costs you the question
- RouterTestingModule is still the recommended way to test routing
- You must stub ActivatedRoute even when using RouterTestingHarness
- navigateByUrl returns before redirects have finished
- routeNativeElement is the root fixture's host element
- The typed navigateByUrl overload returns null when the wrong page loads