Testing Overview
Tests are the safety net that lets you change Angular code with confidence. Angular ships a first-class testing story built on a small set of well-defined tools, and the framework was designed from the start to be testable: dependency injection, the component compiler, and the change detection system all expose seams you can hook into from a test. This page surveys that ecosystem, distinguishes unit tests from integration tests, and explains the central role of the TestBed.
The testing stack
A test needs three layers: a test runner to discover and execute specs, an assertion and spy library to express expectations, and Angular’s own test utilities to instantiate components and services correctly. Knowing which tool owns which job keeps debugging sane.
| Layer | Default (Angular CLI) | Common alternative | Role |
|---|---|---|---|
| Test framework | Jasmine | Jest, Vitest | describe/it blocks, expect, spies, matchers |
| Runner | Karma (legacy) | Jest, Vitest, Web Test Runner | Discovers specs, runs them, reports results |
| Angular utilities | @angular/core/testing | same | TestBed, ComponentFixture, fakeAsync |
For years the CLI scaffolded Jasmine specs run by Karma in a real browser. Modern Angular (17+) is migrating toward Jest and Vitest with an experimental builder, because they run in Node, start faster, and watch incrementally. The good news: TestBed, ComponentFixture, and the rest of @angular/core/testing are framework-agnostic, so the bulk of your test code is identical regardless of the runner underneath.
Jasmine and Jest share nearly the same surface API (
describe,it,expect,spyOn), so switching runners rarely touches your assertions. The differences show up in config, mocking module imports, and how you create spies.
Unit tests vs integration tests
The terms get used loosely, so it helps to anchor them to what a test actually constructs.
A unit test isolates a single class — a service, a pipe, a guard — and replaces its collaborators with fakes. It is fast, deterministic, and pinpoints failures, but it cannot catch wiring mistakes between pieces.
An integration test (sometimes called a component or shallow/deep test) renders a real component with its template and lets several units cooperate. It exercises bindings, the DOM, and child components, catching issues a unit test would miss, at the cost of speed and a noisier failure surface.
| Aspect | Unit test | Integration test |
|---|---|---|
| Scope | One class, mocked deps | Component + template + children |
| Renders a template | No | Yes |
| Speed | Fastest | Slower |
| Catches wiring bugs | No | Yes |
| Typical target | Services, pipes, guards | Components, feature flows |
A healthy suite is mostly unit tests with a focused band of integration tests over the flows that matter most.
The role of TestBed
TestBed is Angular’s testing-time dependency injector and compiler. In production, bootstrapApplication builds the injector and compiles components; in a test, TestBed does the same job in miniature. You declare which providers and components to make available, and TestBed resolves real Angular dependencies (like inject() calls) exactly as they would resolve at runtime.
For a pure service test you often only need to configure providers and pull the instance out:
import { TestBed } from '@angular/core/testing';
import { CartService } from './cart.service';
describe('CartService', () => {
let service: CartService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [CartService],
});
service = TestBed.inject(CartService);
});
it('starts empty', () => {
expect(service.total()).toBe(0);
});
it('adds an item to the running total', () => {
service.add({ id: 1, name: 'Mug', price: 12 });
expect(service.total()).toBe(12);
});
});
For a standalone component you import it and create a ComponentFixture, the handle that wraps the component instance and its rendered DOM. Calling fixture.detectChanges() runs change detection so the template reflects the current state.
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent],
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
fixture.detectChanges();
});
it('increments the displayed count on click', () => {
const button: HTMLButtonElement =
fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Count: 1');
});
});
Standalone components go in imports, not declarations — there is no NgModule to declare them in. Run the suite with the CLI:
ng test
Output:
Chrome 126: Executed 3 of 3 SUCCESS (0.142 secs / 0.118 secs)
TOTAL: 3 SUCCESS
Best practices
- Prefer many fast unit tests over the component, and reserve integration tests for the flows users actually depend on.
- Use
TestBed.inject()to obtain services so they are constructed through Angular’s injector, never vianew. - Call
detectChanges()after every state change you want reflected in the DOM, including the initial render. - Replace real collaborators (HTTP, routers, timers) with test doubles so specs stay deterministic and offline.
- Keep one behavior per
itblock and name it as a sentence — failures should read like a spec, not a stack trace. - Lean on the same assertion API across Jasmine and Jest so a future runner migration leaves your test bodies untouched.