Skip to content
Angular ng testing 4 min read

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.

LayerDefault (Angular CLI)Common alternativeRole
Test frameworkJasmineJest, Vitestdescribe/it blocks, expect, spies, matchers
RunnerKarma (legacy)Jest, Vitest, Web Test RunnerDiscovers specs, runs them, reports results
Angular utilities@angular/core/testingsameTestBed, 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.

AspectUnit testIntegration test
ScopeOne class, mocked depsComponent + template + children
Renders a templateNoYes
SpeedFastestSlower
Catches wiring bugsNoYes
Typical targetServices, pipes, guardsComponents, 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 via new.
  • 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 it block 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.
Last updated June 14, 2026
Was this helpful?