Skip to content
Angular ng testing 4 min read

TestBed Fundamentals

TestBed is the cornerstone of Angular’s testing API. It builds a miniature Angular environment in your test — a dynamically configured module — so you can instantiate components, services, and directives with full dependency injection, change detection, and template rendering. Understanding how to configure that module and how to create a ComponentFixture from it is the foundation that every component and integration test in your suite is built on.

The testing module concept

A real Angular application bootstraps from an ApplicationConfig (or a root module) that wires together providers, imports, and the root component. Tests need the same wiring, but scoped to exactly the piece of code under test. TestBed provides that scope: each test file declares a testing module describing the providers and imports that the thing being tested needs, and nothing more.

This isolation is the point. By configuring a minimal module per test, you control every dependency — swapping a real HttpClient for a test double, providing fake route data, or stubbing a service — so the test exercises only the unit you care about.

import { TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [CounterComponent], // standalone component
    });
  });
});

With standalone components (the default since Angular 17), you put the component in imports, not declarations. The old declarations array is only for components that belong to an NgModule.

Configuring the testing module

TestBed.configureTestingModule accepts a metadata object that mirrors the shape of @NgModule / ApplicationConfig. The keys you reach for most often:

KeyPurpose
importsStandalone components, directives, pipes, and feature modules under test.
providersServices and injection tokens, including test doubles via useValue / useClass.
declarationsNon-standalone components/directives/pipes (legacy NgModule style).
schemasNO_ERRORS_SCHEMA to ignore unknown elements when shallow-rendering.

A typical configuration replaces a real dependency with a stub so the component runs in isolation:

import { TestBed } from '@angular/core/testing';
import { UserCardComponent } from './user-card.component';
import { UserService } from './user.service';

const userServiceStub = {
  getUser: () => ({ id: 1, name: 'Ada Lovelace' }),
};

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [UserCardComponent],
    providers: [{ provide: UserService, useValue: userServiceStub }],
  });
});

Because UserCardComponent uses inject(UserService) internally, the testing module’s provider is what it receives — the stub, not the production service.

Creating a ComponentFixture

Once the module is configured, TestBed.createComponent instantiates a component and returns a ComponentFixture. The fixture is a thin wrapper that gives you access to the component instance, its rendered DOM, and the controls to trigger change detection.

import { TestBed, ComponentFixture } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  let fixture: ComponentFixture<CounterComponent>;
  let component: CounterComponent;

  beforeEach(() => {
    TestBed.configureTestingModule({ imports: [CounterComponent] });
    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges(); // first render
  });

  it('renders the initial count', () => {
    const el: HTMLElement = fixture.nativeElement;
    expect(el.querySelector('.count')?.textContent).toContain('0');
  });

  it('increments when the button is clicked', () => {
    const button = fixture.nativeElement.querySelector('button');
    button.click();
    fixture.detectChanges();
    expect(component.count()).toBe(1);
  });
});

The fixture exposes a small but essential surface:

MemberWhat it gives you
componentInstanceThe live component object — read signals, call methods, set inputs.
nativeElementThe root DOM node, for plain DOM queries and assertions.
debugElementA wrapper with query(By.css(...)) and Angular-aware traversal.
detectChanges()Runs a change detection pass so the template reflects new state.
whenStable()A promise that resolves once pending async tasks settle.

A freshly created fixture has not rendered yet. Call fixture.detectChanges() before asserting on the DOM, or you will query an empty template. The first call also runs ngOnInit.

Change detection in tests

Outside of zone.js, TestBed does not automatically re-render when state changes — you decide exactly when a render happens by calling detectChanges(). This determinism makes tests predictable: you mutate state, you call detectChanges(), then you assert.

If you prefer the framework to handle it, enable automatic change detection. With zoneless or signal-based components this is increasingly common:

import { provideExperimentalZonelessChangeDetection } from '@angular/core';

TestBed.configureTestingModule({
  imports: [CounterComponent],
  providers: [provideExperimentalZonelessChangeDetection()],
});

Setting a signal input via fixture.componentRef.setInput('value', 42) followed by fixture.detectChanges() is the canonical way to drive an input-bound component through its states.

Output:

CounterComponent
  ✓ renders the initial count
  ✓ increments when the button is clicked

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

Best Practices

  • Configure the smallest possible testing module — import only what the unit under test actually needs, and stub everything else.
  • Put standalone components in imports; reserve declarations for legacy NgModule-based code.
  • Call fixture.detectChanges() after every state change you want reflected in the DOM, and once before your first DOM assertion.
  • Prefer useValue/useClass test doubles over the real service to keep tests fast and deterministic.
  • Use fixture.componentRef.setInput() to set inputs rather than assigning to the component instance directly, so input transforms and signal inputs behave correctly.
  • Reach for debugElement.query(By.css(...)) over raw querySelector when you need Angular-aware DOM traversal.
Last updated June 14, 2026
Was this helpful?