Angular · Production

Testing

Angular is built for testing: dependency injection makes services easy to swap, and TestBed renders components in a test. A few patterns cover most of what you need.

Advanced 6 min Lesson 34 of 38Examples target Angular 20 with standalone components. Each lesson states the version it was verified against.
Before you start:ComponentsHttpClient
0/38 in Angular
On this page · 6 min · Advanced

Learning objectives#

  • Configure TestBed for a standalone component
  • Interact with the DOM and assert results
  • Test HTTP services with HttpTestingController
  • Replace dependencies with test doubles

Explanation#

ng test runs unit tests (the default runner in Angular 20 projects is Karma with Jasmine; newer CLI versions also support Vitest). Spec files sit next to the code: product-list.spec.ts.

TestBed.configureTestingModule({ imports: [MyComponent], providers: [...] }) prepares a mini app; TestBed.createComponent(MyComponent) renders it. fixture.detectChanges() (or await fixture.whenStable()) applies changes.

Find elements with fixture.nativeElement.querySelector(...), click them, then assert on text. Test what users see, not private fields.

For HTTP services: provideHttpClient() + provideHttpClientTesting(), then HttpTestingController.expectOne(url) checks the request and flush(data) returns a fake response.

Swap dependencies with providers: { provide: CartService, useValue: fakeCart } — the payoff of using DI everywhere.

Syntax#

TypeScript Illustrative
await TestBed.configureTestingModule({ imports: [Counter] }).compileComponents();
const fixture = TestBed.createComponent(Counter);
fixture.detectChanges();
fixture.nativeElement.querySelector('button').click();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('1');

Code example: Specs for a component and an HTTP service#

TypeScriptproduct.spec.ts Illustrative
import { Component, Injectable, inject, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';


// ── Code under test ──
export type Product = { id: number; name: string };


@Injectable({ providedIn: 'root' })
export class ProductApi {
  private http = inject(HttpClient);
  list() { return this.http.get<Product[]>('/api/products'); }
}


@Component({
  selector: 'app-like',
  template: `<button (click)="like()">Like</button><span>{{ likes() }}</span>`,
})
export class LikeButton {
  likes = signal(0);
  like() { this.likes.update(n => n + 1); }
}


// ── Component test ──
describe('LikeButton', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({ imports: [LikeButton] }).compileComponents();
  });


  it('counts clicks', () => {
    const fixture = TestBed.createComponent(LikeButton);
    fixture.detectChanges();
    const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
    button.click();
    button.click();
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('span').textContent).toBe('2');
  });
});


// ── HTTP service test ──
describe('ProductApi', () => {
  let api: ProductApi;
  let http: HttpTestingController;


  beforeEach(() => {
    TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
    api = TestBed.inject(ProductApi);
    http = TestBed.inject(HttpTestingController);
  });


  afterEach(() => http.verify());          // no unexpected requests


  it('GETs /api/products', () => {
    let result: Product[] = [];
    api.list().subscribe(r => (result = r));


    const req = http.expectOne('/api/products');
    expect(req.request.method).toBe('GET');
    req.flush([{ id: 1, name: 'Pen' }]);


    expect(result.length).toBe(1);
    expect(result[0].name).toBe('Pen');
  });
});
Expected output
ng test reports 2 specs, 0 failures: “LikeButton counts clicks” and “ProductApi GETs /api/products”. No real server is contacted — HttpTestingController answers the request.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. TestBed imports the standalone component and creates it with a fixture.
  2. Clicking the real button and calling detectChanges updates the DOM; the test checks the visible text.
  3. provideHttpClientTesting replaces the real backend with a controllable fake.
  4. expectOne asserts that exactly one matching request was made; flush sends the fake response.
  5. http.verify() fails the test if any request was not handled.

Real-world use case#

A team requires a spec for every service and every component with logic. CI runs ng test --watch=false on each pull request, catching broken bindings and API contract changes early.

Common mistakes#

Forgetting detectChanges

Without fixture.detectChanges() the template is not rendered or updated, and assertions see old values.

Calling real APIs in unit tests

Tests become slow and flaky. Use HttpTestingController or mocks.

Testing private implementation

Asserting on private fields breaks with refactors. Assert on DOM output and public behaviour.

Best practices#

  • Test what users see and services’ public APIs
  • Mock HTTP with HttpTestingController
  • Swap dependencies through providers
  • Run tests in CI on every change

Remember this#

TestBed.configureTestingModule → createComponent → detectChanges.

HTTP: provideHttpClientTesting + expectOne + flush.

DI makes swapping dependencies trivial.

Practice exercise#

Your task: Write a spec that provides a fake GreetingService (useValue with greet() returning "Hi test") to a component that displays service.greet(), and checks the text.

Need a hint?
providers: [{ provide: GreetingService, useValue: { greet: () => "Hi test" } }].
Show solution
TypeScript Illustrative
import { Component, Injectable, inject } from '@angular/core';
import { TestBed } from '@angular/core/testing';


@Injectable({ providedIn: 'root' })
export class GreetingService {
  greet() { return 'Hello from the real service'; }
}


@Component({ selector: 'app-greeting', template: `<p>{{ service.greet() }}</p>` })
export class Greeting {
  protected service = inject(GreetingService);
}


describe('Greeting', () => {
  it('uses the injected service', async () => {
    await TestBed.configureTestingModule({
      imports: [Greeting],
      providers: [{ provide: GreetingService, useValue: { greet: () => 'Hi test' } }],
    }).compileComponents();


    const fixture = TestBed.createComponent(Greeting);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('Hi test');
  });
});

Mini quiz#

Testing quizMultiple choice Question 1 of 3

Which call updates the template in a component test?

Score so far: 0/0

Want more? Practise all Angular MCQs · Angular interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.