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:
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 testsRouter
: The Angular router service that we'll use to navigateLocation
: Helps verify the current URL pathfakeAsync
andtick
: 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:
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:
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:
- We use
router.navigate()
to programmatically navigate to a route tick()
simulates the passage of time, allowing async operations to complete- We assert that
location.path()
matches our expected URL
Testing Routes with Parameters
Testing routes with parameters is a common scenario:
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:
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');
}));
Testing Router Links
Often, you'll want to test that router links in your templates work correctly:
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:
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:
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:
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:
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:
// ❌ 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:
// 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
- Official Angular Router Testing Guide
- Angular Router Documentation
- Testing Angular Applications - Book by Jesse Palmer et al.
Practice Exercises
- Create tests for a route guard that checks user roles before allowing access
- Write tests for a multi-step form with routing between steps
- Test a scenario with nested routes and child components
- 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! :)