In the first testing lesson you tested a counter and a store. Real apps need tests for guards, interceptors, navigation and components with inputs and outputs. These tests catch the bugs that matter most: broken login redirects, missing headers and wrong events.
All the tests in this lesson run with ng test --watch=false and use the code from earlier lessons.
What you will learn
- How to test a guard with
RouterTestingHarness - How to test an interceptor with
HttpTestingController - How to test inputs, outputs and mocked services
Test the auth guard
The guard from the route guards lesson redirects to /login and remembers the wanted URL. Test it with a real router and fake pages:
// src/app/auth-guard.spec.ts
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { provideRouter, Router } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { Auth } from './auth/auth';
import { authGuard } from './auth-guard';
@Component({ template: 'secret page' })
class SecretPage {}
@Component({ template: 'login page' })
class LoginPage {}
describe('authGuard', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'secret', component: SecretPage, canActivate: [authGuard] },
{ path: 'login', component: LoginPage },
]),
],
});
});
it('redirects to login and keeps the wanted url when signed out', async () => {
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/secret');
const router = TestBed.inject(Router);
expect(router.url).toBe('/login?returnUrl=%2Fsecret');
});
it('opens the page when signed in', async () => {
TestBed.inject(Auth).login('Ada');
const harness = await RouterTestingHarness.create();
const page = await harness.navigateByUrl('/secret', SecretPage);
expect(page).toBeInstanceOf(SecretPage);
expect(TestBed.inject(Router).url).toBe('/secret');
});
});
RouterTestingHarness creates a router outlet for you, navigates, and returns the component that was activated. No browser, no flaky waiting.
Test an interceptor
// src/app/http/interceptors.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { Auth } from '../auth/auth';
import { authInterceptor, loadingInterceptor } from './interceptors';
import { Loading } from './loading';
describe('interceptors', () => {
let http: HttpClient;
let controller: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([loadingInterceptor, authInterceptor])),
provideHttpClientTesting(),
],
});
http = TestBed.inject(HttpClient);
controller = TestBed.inject(HttpTestingController);
});
afterEach(() => controller.verify());
it('adds the auth header only to our own API', () => {
TestBed.inject(Auth).login('Ada');
http.get('https://api.example.com/items').subscribe();
http.get('https://other.example.org/items').subscribe();
const own = controller.expectOne('https://api.example.com/items');
const other = controller.expectOne('https://other.example.org/items');
expect(own.request.headers.get('Authorization')).toContain('Bearer');
expect(other.request.headers.has('Authorization')).toBe(false);
own.flush([]);
other.flush([]);
});
it('turns the loading flag on during a request and off afterwards', () => {
const loading = TestBed.inject(Loading);
http.get('https://api.example.com/items').subscribe();
expect(loading.active()).toBe(true);
controller.expectOne('https://api.example.com/items').flush([]);
expect(loading.active()).toBe(false);
});
it('turns the loading flag off even when the request fails', () => {
const loading = TestBed.inject(Loading);
http.get('https://api.example.com/items').subscribe({ error: () => {} });
controller
.expectOne('https://api.example.com/items')
.flush('boom', { status: 500, statusText: 'Server Error' });
expect(loading.active()).toBe(false);
});
});
The second and third tests protect the finalize behaviour from the interceptors lesson.
Test inputs and outputs
Use fixture.componentRef.setInput() to set inputs, as the template would, and subscribe to outputs:
// src/app/features/products/ui/product-card.spec.ts
import { TestBed } from '@angular/core/testing';
import { ProductCard } from './product-card';
describe('ProductCard', () => {
async function create() {
const fixture = TestBed.createComponent(ProductCard);
fixture.componentRef.setInput('product', { id: 7, name: 'Lamp', price: 25 });
await fixture.whenStable();
return { fixture, el: fixture.nativeElement as HTMLElement };
}
it('renders the product', async () => {
const { el } = await create();
expect(el.textContent).toContain('Lamp');
expect(el.textContent).toContain('25 USD');
});
it('emits the product id when the button is clicked', async () => {
const { fixture, el } = await create();
const emitted: number[] = [];
fixture.componentInstance.add.subscribe((id: number) => emitted.push(id));
el.querySelector('button')!.click();
expect(emitted).toEqual([7]);
});
});
Mock a service in a page test
// src/app/features/products/pages/products-page.spec.ts
import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ProductsFacade } from '../data/products.facade';
import { ProductsPage } from './products-page';
describe('ProductsPage', () => {
it('shows products from the facade and forwards add events', async () => {
const added: number[] = [];
const fake = {
products: signal([{ id: 1, name: 'Pen', price: 2 }]),
cartCount: signal(0),
addToCart: (id: number) => added.push(id),
};
TestBed.configureTestingModule({ providers: [{ provide: ProductsFacade, useValue: fake }] });
const fixture = TestBed.createComponent(ProductsPage);
await fixture.whenStable();
const el = fixture.nativeElement as HTMLElement;
expect(el.textContent).toContain('Pen');
el.querySelector('button')!.click();
expect(added).toEqual([1]);
fake.cartCount.set(3);
await fixture.whenStable();
expect(el.textContent).toContain('3 in cart');
});
});
Because the page depends on the facade abstraction, the test supplies a small fake made of signals, and then changes a signal to see the template react.
Habits of good tests
- Test behaviour, not implementation. Click and read the text instead of calling private methods.
- One reason to fail per test, with a name that reads as a sentence.
- No real network, timers or randomness. Fake them.
- Cover failure paths: errors, empty lists, double clicks.
- Keep tests fast. Unit tests should run in seconds, so developers run them all the time.
- Do not chase 100% coverage. Aim at logic that would hurt if it broke. Run
ng test --watch=false --coverageto see gaps. - For complex widgets, the Angular CDK offers component harnesses, which give tests a stable API that survives markup changes.
Try it yourself
- Write a test for
Auththat checksisLoggedInbefore and afterloginandlogout. - Add a test that the error interceptor re-throws the original error to the caller.
- Test a
canMatchguard from the advanced routing lesson withRouterTestingHarness.
Quick check
- Which tool lets you test navigation with a real router?
- How do you set a signal input in a test?
- Why replace a service with a fake in a component test?
Next, test the whole app like a user in end-to-end tests with Playwright. See the testing guide.