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, notdeclarations. The olddeclarationsarray is only for components that belong to anNgModule.
Configuring the testing module
TestBed.configureTestingModule accepts a metadata object that mirrors the shape of @NgModule / ApplicationConfig. The keys you reach for most often:
| Key | Purpose |
|---|---|
imports | Standalone components, directives, pipes, and feature modules under test. |
providers | Services and injection tokens, including test doubles via useValue / useClass. |
declarations | Non-standalone components/directives/pipes (legacy NgModule style). |
schemas | NO_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:
| Member | What it gives you |
|---|---|
componentInstance | The live component object — read signals, call methods, set inputs. |
nativeElement | The root DOM node, for plain DOM queries and assertions. |
debugElement | A 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 runsngOnInit.
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; reservedeclarationsfor legacyNgModule-based code. - Call
fixture.detectChanges()after every state change you want reflected in the DOM, and once before your first DOM assertion. - Prefer
useValue/useClasstest 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 rawquerySelectorwhen you need Angular-aware DOM traversal.