Skip to main content

Angular Router Testing

Introduction

Angular's router is a powerful system that enables navigation between different components in an application based on the browser URL. Testing routing functionality is crucial to ensure that your application navigates correctly and renders the appropriate components when users visit specific URLs.

This guide will walk you through testing Angular's routing capabilities, from simple route navigation tests to more complex scenarios involving route parameters, guards, and resolvers.

Prerequisites

Before diving into router testing, you should have:

  • Basic knowledge of Angular
  • Understanding of Angular's routing system
  • Familiarity with Angular testing fundamentals

Setting Up for Router Testing

Angular provides specialized testing utilities for router testing in the @angular/router/testing package.

Required Imports

Let's start by understanding the key imports needed for router testing:

typescript
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
  • RouterTestingModule: Provides a simulated routing environment for tests
  • Router: The Angular router service that we'll use to navigate
  • Location: Helps verify the current URL path
  • fakeAsync and tick: Utilities to manage async operations in tests

Basic Router Test Setup

Creating a Test Module

Here's how to set up a basic test module with routing:

typescript
describe('Router Testing', () => {
let router: Router;
let location: Location;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'products/:id', component: ProductDetailComponent }
]),
// Other necessary modules
],
declarations: [
HomeComponent,
AboutComponent,
ProductDetailComponent,
AppComponent
]
});

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

// Tests will be added here
});

Testing Basic Navigation

Let's write tests for basic navigation between routes:

typescript
it('should navigate to the default path (empty string)', fakeAsync(() => {
router.navigate(['']);
tick(); // Process navigation
expect(location.path()).toBe('/');
}));

it('should navigate to /about', fakeAsync(() => {
router.navigate(['/about']);
tick();
expect(location.path()).toBe('/about');
}));

Here's what happens in these tests:

  1. We use router.navigate() to programmatically navigate to a route
  2. tick() simulates the passage of time, allowing async operations to complete
  3. We assert that location.path() matches our expected URL

Testing Routes with Parameters

Testing routes with parameters is a common scenario:

typescript
it('should navigate to product details with an id parameter', fakeAsync(() => {
const productId = '123';
router.navigate(['/products', productId]);
tick();
expect(location.path()).toBe('/products/123');
}));

Integration Testing with Router

Let's create a more realistic test that checks if the component actually changes when we navigate:

typescript
it('should render the AboutComponent when navigating to /about', fakeAsync(() => {
// Create root component
const fixture = TestBed.createComponent(AppComponent);

// Navigate to about page
router.navigate(['/about']);
tick();
fixture.detectChanges();

// Check if AboutComponent content is displayed
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('About Page');
}));

Often, you'll want to test that router links in your templates work correctly:

typescript
it('should navigate when a link is clicked', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();

const links = fixture.nativeElement.querySelectorAll('a');
const aboutLink = Array.from(links).find(link => link.textContent === 'About');

aboutLink.click();
tick();
fixture.detectChanges();

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

Testing Route Guards

Route guards protect routes from unauthorized access. Testing them requires additional setup:

typescript
describe('AuthGuard', () => {
let authGuard: AuthGuard;
let authService: AuthService;
let router: Router;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuthGuard,
{
provide: AuthService,
useValue: {
isAuthenticated: jasmine.createSpy('isAuthenticated')
}
},
{
provide: Router,
useValue: {
navigate: jasmine.createSpy('navigate')
}
}
]
});

authGuard = TestBed.inject(AuthGuard);
authService = TestBed.inject(AuthService);
router = TestBed.inject(Router);
});

it('should allow access when user is authenticated', () => {
(authService.isAuthenticated as jasmine.Spy).and.returnValue(true);
expect(authGuard.canActivate({} as any, {} as any)).toBe(true);
});

it('should redirect to login when user is not authenticated', () => {
(authService.isAuthenticated as jasmine.Spy).and.returnValue(false);
expect(authGuard.canActivate({} as any, {} as any)).toBe(false);
expect(router.navigate).toHaveBeenCalledWith(['/login']);
});
});

Testing Route Resolvers

Route resolvers fetch data before a route activates. Here's how to test them:

typescript
describe('ProductResolver', () => {
let resolver: ProductResolver;
let productService: ProductService;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ProductResolver,
{
provide: ProductService,
useValue: {
getProduct: jasmine.createSpy('getProduct')
}
}
]
});

resolver = TestBed.inject(ProductResolver);
productService = TestBed.inject(ProductService);
});

it('should resolve product data', () => {
const mockProduct = { id: '123', name: 'Test Product' };
(productService.getProduct as jasmine.Spy).and.returnValue(of(mockProduct));

const route = { params: { id: '123' } } as any;

let result: any;
resolver.resolve(route).subscribe(data => result = data);

expect(productService.getProduct).toHaveBeenCalledWith('123');
expect(result).toEqual(mockProduct);
});
});

Real-world Example: E-commerce Navigation Flow

Let's create a more comprehensive test that verifies a common e-commerce navigation flow:

typescript
it('should handle a complete shopping flow', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();

// Step 1: Navigate to products list
router.navigate(['/products']);
tick();
fixture.detectChanges();
expect(location.path()).toBe('/products');

// Step 2: Select a product
const productLinks = fixture.nativeElement.querySelectorAll('.product-link');
productLinks[0].click();
tick();
fixture.detectChanges();

// Verify we're on a product detail page
expect(location.path()).toMatch(/\/products\/\d+/);

// Step 3: Add to cart and go to checkout
const addToCartBtn = fixture.nativeElement.querySelector('.add-to-cart');
addToCartBtn.click();
fixture.detectChanges();

const checkoutBtn = fixture.nativeElement.querySelector('.checkout');
checkoutBtn.click();
tick();
fixture.detectChanges();

// Verify we're on the checkout page
expect(location.path()).toBe('/checkout');
}));

Testing Outlet Activation and Deactivation

If your app uses multiple router outlets, you can test them too:

typescript
it('should activate and deactivate secondary outlets', fakeAsync(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: 'home', component: HomeComponent },
{ path: 'sidebar', component: SidebarComponent, outlet: 'sidebar' }
])
],
declarations: [HomeComponent, SidebarComponent, AppComponent]
});

router = TestBed.inject(Router);

const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();

// Navigate to primary outlet
router.navigate(['/home']);
tick();

// Navigate to secondary outlet
router.navigate([{ outlets: { sidebar: 'sidebar' } }]);
tick();
fixture.detectChanges();

// Check if both outlets are active
const sidebarOutlet = fixture.nativeElement.querySelector('router-outlet[name="sidebar"] + *');
expect(sidebarOutlet).toBeTruthy();

// Close secondary outlet
router.navigate([{ outlets: { sidebar: null } }]);
tick();
fixture.detectChanges();

// Verify sidebar is gone
const emptySidebar = fixture.nativeElement.querySelector('router-outlet[name="sidebar"] + *');
expect(emptySidebar).toBeFalsy();
}));

Common Pitfalls and Solutions

Asynchronous Testing Issues

Remember to use fakeAsync and tick() to handle asynchronous operations:

typescript
// ❌ Incorrect - test may complete before navigation finishes
it('should navigate', () => {
router.navigate(['/about']);
expect(location.path()).toBe('/about'); // May fail!
});

// ✅ Correct - waits for navigation to complete
it('should navigate', fakeAsync(() => {
router.navigate(['/about']);
tick();
expect(location.path()).toBe('/about');
}));

Testing Lazy Loaded Modules

Lazy loaded modules require special handling:

typescript
// In your test configuration
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
])
]
});

// In your test
it('should load admin module lazily', fakeAsync(() => {
router.navigate(['/admin']);
tick(); // For route navigation
tick(); // For lazy module loading
expect(location.path()).toBe('/admin');
}));

Summary

Router testing is a critical part of ensuring your Angular application's navigation works correctly. In this guide, we've covered:

  • Setting up your testing environment for router tests
  • Testing basic navigation between routes
  • Testing routes with parameters
  • Verifying component rendering after navigation
  • Testing route guards and resolvers
  • Handling common pitfalls in router testing

By thoroughly testing your application's routing, you can ensure that users navigate through your app correctly and see the right content at the right time.

Additional Resources

Practice Exercises

  1. Create tests for a route guard that checks user roles before allowing access
  2. Write tests for a multi-step form with routing between steps
  3. Test a scenario with nested routes and child components
  4. Create tests for URL parameter changes without navigation (using router.serializeUrl())


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)