Testing components and services

Angular · Advanced · 15 min

Tests keep large Angular apps maintainable; TestBed is the standard way to test with real DI and templates.

New Angular projects use Vitest as the unit test runner (older ones use Karma and Jasmine; the APIs below are the same). Run tests with ng test.

TestBed creates a testing module with Angular's DI and compiler:

  • Services: TestBed.inject(MyService) after TestBed.configureTestingModule({ providers: [...] }). Replace dependencies with fakes using { provide: Real, useValue: fake }.
  • Components: const fixture = TestBed.createComponent(MyComponent); fixture.componentInstance is the class, fixture.nativeElement is the DOM.
  • Set inputs with fixture.componentRef.setInput('name', 'Ada').
  • In zoneless apps, `await fixture.whenStable()` waits for change detection and pending work after you change state or trigger events; fixture.detectChanges() forces a synchronous pass.

For HTTP, provide provideHttpClient() and provideHttpClientTesting(), then use `HttpTestingController`: httpMock.expectOne('/api/todos').flush([...]) returns fake data, and httpMock.verify() checks there were no unexpected requests.

Test behaviour as a user sees it: query the DOM, click buttons (button.click()), assert rendered text. Component harnesses (from the CDK) give a stable API for interacting with complex components like Angular Material's.

Pure logic (pipes, utility functions, computed-heavy services) can be tested as plain classes without TestBed. For end-to-end tests, use Playwright or Cypress.

Example

import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { UserDetail } from './user-detail';          // the httpResource example component

describe('UserDetail', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
  });

  it('renders the loaded user', async () => {
    const fixture = TestBed.createComponent(UserDetail);
    const http = TestBed.inject(HttpTestingController);
    await fixture.whenStable();

    http.expectOne('https://jsonplaceholder.typicode.com/users/1')
        .flush({ id: 1, name: 'Ada', email: 'ada@example.com' });
    await fixture.whenStable();

    const el: HTMLElement = fixture.nativeElement;
    expect(el.querySelector('h2')?.textContent).toContain('Ada');
    http.verify();                                   // no unexpected requests
  });
});

Remember

  • TestBed.configureTestingModule + createComponent/inject for tests with real DI
  • Use setInput for inputs and await fixture.whenStable() in zoneless apps
  • provideHttpClientTesting + HttpTestingController fake HTTP responses

Watch out for

  • Asserting on the DOM right after changing state without awaiting whenStable() or calling detectChanges().

Check yourself

  1. How do you set a signal input named 'user' on a component under test?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/testing