Testing Angular Routing and Navigation with RouterTestingModule

Angular applications often put important behaviour in the router. A product page may depend on an ID in the URL, an admin screen may require authentication, and a search page may preserve filters in query parameters. If those paths are tested only through a browser, failures can be slow to diagnose and difficult to reproduce.

Unit tests can exercise route configuration, navigation events, redirects, guards, resolvers and activated components without launching a full end-to-end session. Angular’s RouterTestingModule has traditionally been the standard way to replace the production router with a test-friendly configuration.

For developers working on applications used across Sydney, Melbourne, Brisbane or regional Australia, reliable navigation is particularly valuable. A test suite can catch broken deep links before a release reaches customers using different devices, networks and time zones, including slower NBN connections or mobile networks outside the capital cities.

Why Routing Tests Matter

A routing test verifies the application’s contract with the browser. It can confirm that /orders/42 activates the order detail component, that /login appears when an unauthenticated visitor requests /account, and that query parameters survive a search or sorting action. These are user-visible behaviours, even though the tests run without rendering an entire site.

Routing errors can also affect more than navigation. An incorrect route order may send a valid URL to a wildcard page. A missing parameter may trigger an API call with undefined. A guard that returns the wrong URL tree can create a redirect loop. Testing these cases close to the routing configuration gives developers a focused failure instead of a vague browser error.

A route-focused suite fits naturally alongside component tests in a .NET and Angular codebase. The same discipline described in the development articles on Kilt and Code applies here: isolate the behaviour under test, make dependencies explicit and keep the test scenario easy to read.

Set Up RouterTestingModule

In applications using the classic Angular testing style, import RouterTestingModule from @angular/router/testing and provide the routes needed for the scenario. withRoutes keeps the test independent from the complete application route tree, which makes the test faster and reduces unrelated setup.

import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { Router, RouterTestingModule } from '@angular/router/testing';

@Component({ template: 'Home' })
class HomeComponent {}

@Component({ template: 'Profile' })
class ProfileComponent {}

describe('application routing', () => {
  let router: Router;
  let location: Location;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [HomeComponent, ProfileComponent],
      imports: [
        RouterTestingModule.withRoutes([
          { path: '', component: HomeComponent },
          { path: 'profile', component: ProfileComponent }
        ])
      ]
    });

    router = TestBed.inject(Router);
    location = TestBed.inject(Location);
  });
});

The test should normally include a host component with a router-outlet when it needs to verify which component is displayed. Testing the URL alone can confirm navigation, but it does not prove that Angular activated the expected view.

@Component({
  template: '<router-outlet></router-outlet>'
})
class TestHostComponent {}

Add TestHostComponent to the declarations and create it with TestBed.createComponent. Calling fixture.detectChanges() allows the router outlet to connect to the configured router. This setup is useful for route activation tests, while smaller tests can inject Router and Location directly.

Drive Navigation And Inspect The URL

The simplest navigation test calls router.navigate and waits for its promise to resolve. Angular navigation is asynchronous because guards, resolvers and route transitions may perform work before the URL changes.

it('navigates to the profile page', fakeAsync(() => {
  router.navigate(['/profile']);
  tick();

  expect(location.path()).toBe('/profile');
}));

fakeAsync and tick are common in older Angular test suites, although the exact timing can depend on the application and test runner. An alternative is to return the navigation promise:

it('updates the browser path', async () => {
  await router.navigate(['/profile']);

  expect(location.path()).toBe('/profile');
});

When a navigation starts from a component, test the same interaction a user performs. Find the link through the host fixture, trigger a click, run change detection and then inspect the resulting path. This catches an incorrect routerLink, a typo in a route segment or a link that accidentally reloads the page.

For Australian commerce applications, this can protect routes such as /au/products, /checkout, and /stores/melbourne. A small path mismatch may affect a state-specific promotion or prevent customers from returning to a saved cart after a public holiday campaign.

Assert Parameters And Query Strings

Dynamic segments should be tested with realistic values and with the component that consumes them. A route such as { path: 'customers/:id', component: CustomerComponent } should activate with a known ID, and the component should read that value from ActivatedRoute.

await router.navigate(['/customers', 1842]);

expect(location.path()).toBe('/customers/1842');

A component test can mock ActivatedRoute, but an integration-style routing test is often better when the goal is to verify the route definition and parameter delivery together. Subscribe to paramMap or use the snapshot, depending on how the component is designed.

Query parameters deserve separate assertions because they are not path segments. Use navigation extras to test filters, pagination and sorting:

await router.navigate(['/products'], {
  queryParams: {
    category: 'tools',
    page: 2
  }
});

expect(location.path()).toBe('/products?category=tools&page=2');

If parameter order is not part of the contract, avoid brittle string comparisons. Inspect the router state or use a URL tree and assert the meaningful values. This is especially helpful when a search page adds tracking parameters, preserves a postcode, or handles Australian state selections such as NSW, Victoria or Queensland.

Test Guards Redirects And Failures

Guards are easiest to test with a small route table and a controlled authentication service. Configure a protected route, make the fake service return the desired state, then assert whether navigation succeeds or redirects.

const routes = [
  { path: 'login', component: LoginComponent },
  {
    path: 'account',
    canActivate: [authGuard],
    component: AccountComponent
  }
];

A modern functional guard may return true, false or a UrlTree. Returning a UrlTree is generally preferable to calling router.navigate inside the guard because Angular can complete one controlled navigation. The test should verify both the final URL and, where relevant, the navigation result.

await router.navigate(['/account']);

expect(location.path()).toBe('/login');

Also test unknown URLs and route ordering. A wildcard route should generally be last, and a test can prove that a recognised path is not swallowed by **. Resolver failures deserve their own case because an error may cancel navigation, redirect to an error page or be handled by a global error strategy.

Avoid making every route test depend on real HTTP calls. Replace data services with spies or test providers, then reserve end-to-end tests for verifying the complete server and browser interaction. This separation keeps failures useful in both a Melbourne development office and a distributed team working across Perth and Adelaide.

Use Modern Angular Router Test APIs

RouterTestingModule remains relevant when maintaining an existing Angular application, but Angular’s current guidance favours provideRouter and RouterTestingHarness for many new tests. In recent Angular versions, RouterTestingModule is deprecated, so teams should understand the newer approach before adding it to a fresh standalone application.

RouterTestingHarness makes it easier to navigate to a route and inspect the routed component. A test can create the harness, navigate by URL and receive the component instance associated with the active route.

import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [
      provideRouter([
        { path: 'profile', component: ProfileComponent }
      ])
    ]
  });
});

it('activates the profile component', async () => {
  const harness = await RouterTestingHarness.create();
  const component = await harness.navigateByUrl(
    '/profile',
    ProfileComponent
  );

  expect(component).toBeInstanceOf(ProfileComponent);
});

The harness reduces the need for a hand-written host component and makes the intended route explicit. It is a good fit for standalone components and newer test setups, while RouterTestingModule can remain in legacy NgModule-based suites until the surrounding test infrastructure is migrated. Articles such as Oscar Sanchez's testing notes can also provide useful context when comparing testing styles across Angular projects.

Useful choices when selecting a router test style include:

Keep Navigation Specs Stable

A reliable routing suite tests observable behaviour rather than Angular’s internal implementation. Prefer assertions about the final URL, activated component, rendered link and navigation result. Avoid checking private router fields or relying on incidental event ordering unless the event sequence is itself a requirement.

Reset the test environment between cases and provide only the services required for the route. Fake authentication, API clients, feature flags and storage so that a routing test does not depend on a previous spec. When using signals or observables, ensure the test advances the relevant asynchronous work before making assertions.

A useful suite commonly covers these scenarios:

Use descriptive test names that explain the user-facing rule. “Redirects an anonymous visitor from account to login” communicates more than “guard works”. This clarity helps during release preparation for Australian retail events, end-of-financial-year changes and campaigns that may send large volumes of visitors directly to deep links.

Run routing tests in the same continuous integration pipeline as component and service tests. They should not require Chrome, a running API or a particular local time zone. A deterministic suite gives a team confidence that navigation still works when a developer is travelling between Sydney and Canberra, working remotely in regional New South Wales or updating dependencies before a production deployment.

A routing test suite is most valuable when it reflects the routes customers actually use. Start with the application’s primary paths, then add cases for parameters, query strings, access control, redirects and unknown URLs. Keep the route configuration small in each spec, name the expected behaviour clearly and migrate gradually to modern router providers where appropriate.

Add these tests alongside the feature rather than waiting for a navigation bug in production. With RouterTestingModule in legacy suites or RouterTestingHarness in current Angular projects, you can turn routing rules into executable documentation and make every release safer for users across Australia.