Testing

This section documents modern, standalone Angular — signals, the built-in @if / @for / @switch control flow, @defer, typed reactive forms, provideHttpClient, functional guards and interceptors, and server-side rendering with hydration — as described by the official documentation at angular.dev, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted.

This section’s bibliography lists the reference material consulted while preparing these pages.

TestBed builds a miniature module around the unit under test, and ComponentFixture gives access to the instance and its rendered DOM. The default runner is Karma with Jasmine; an official migration to Vitest is under way. See Testing.

Anatomy of a component test

TestBed.configureTestingModule() declares the standalone components, imports, and providers the test needs; TestBed.createComponent() returns a ComponentFixture.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Counter } from './counter';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [Counter],          // standalone component
    }).compileComponents();

    fixture = TestBed.createComponent(Counter);
    component = fixture.componentInstance;
    fixture.detectChanges();       // run the first change-detection pass
  });

  it('increments when the button is clicked', () => {
    const button = fixture.debugElement.query(By.css('button'));
    button.triggerEventHandler('click');
    fixture.detectChanges();

    expect(component.count()).toBe(1);
    expect(fixture.nativeElement.textContent).toContain('Count: 1');
  });
});
  • fixture.detectChanges() runs change detection — call it after every state change you want reflected in the DOM; fixture.autoDetectChanges() does it for you.

  • fixture.componentInstance is the class instance; fixture.nativeElement is the host DOM node.

  • fixture.debugElement wraps the rendered tree — query(By.css(…​)), queryAll(By.css(…​)), and By.directive(…​) find nodes without touching the real DOM API.

Components with inputs and outputs

fixture.componentRef.setInput() sets a signal or decorator input; subscribe to an output() like an observable.

it('emits selected on click', () => {
  const fixture = TestBed.createComponent(ProductCard);
  fixture.componentRef.setInput('product', { id: 7, name: 'Widget' });
  fixture.detectChanges();

  const selected = jasmine.createSpy('selected');
  fixture.componentInstance.selected.subscribe(selected);

  fixture.debugElement.query(By.css('button')).triggerEventHandler('click');
  expect(selected).toHaveBeenCalledWith(7);
});

Replace a dependency with a stub through the providers array, and spy on its methods:

class FakeCart { add = jasmine.createSpy('add'); }

TestBed.configureTestingModule({
  imports: [ProductCard],
  providers: [{ provide: CartService, useClass: FakeCart }],
});

jasmine.createSpy() makes a standalone spy; spyOn(obj, 'method').and.returnValue(…​) wraps an existing method. See Component testing scenarios.

Services and HTTP

Test a plain service by pulling it from the injector:

TestBed.configureTestingModule({ providers: [PriceService] });
const service = TestBed.inject(PriceService);
expect(service.withTax(100)).toBe(121);

For services that call HttpClient, combine provideHttpClient() with provideHttpClientTesting() and drive requests through HttpTestingController:

import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';

let http: HttpTestingController;

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [ProductApi, provideHttpClient(), provideHttpClientTesting()],
  });
  http = TestBed.inject(HttpTestingController);
});

afterEach(() => http.verify());     // fail if any request is still outstanding

it('fetches a product', () => {
  const api = TestBed.inject(ProductApi);
  let result: Product | undefined;
  api.get(7).subscribe((p) => (result = p));

  const req = http.expectOne('/api/products/7');
  expect(req.request.method).toBe('GET');
  req.flush({ id: 7, name: 'Widget' });   // resolve the request

  expect(result?.name).toBe('Widget');
});

Asynchronous testing

  • fakeAsync + tick(ms) / flush() — run timers and microtasks synchronously on a virtual clock. tick(1000) advances time; flush() drains every pending timer.

  • waitForAsync + fixture.whenStable() — wait for real promises and XHR to settle, then assert.

import { fakeAsync, tick } from '@angular/core/testing';

it('debounces the search', fakeAsync(() => {
  component.search('ab');
  component.search('abc');
  tick(300);
  expect(component.results()).toHaveSize(1);
}));

Pipes and attribute directives

A pipe is a plain class — call transform directly:

expect(new TruncatePipe().transform('a long string', 6)).toBe('a long…');

Test an attribute directive by applying it to a small host component and asserting the effect on the host element:

@Component({ imports: [HighlightDirective], template: `<p appHighlight="gold">Hi</p>` })
class Host {}

const fixture = TestBed.createComponent(Host);
fixture.detectChanges();
const p = fixture.debugElement.query(By.css('p'));
expect(p.styles['background-color']).toBe('gold');

Testing the router

Provide a router with provideRouter([…​]) and navigate with RouterTestingHarness. Run a functional guard or resolver inside TestBed.runInInjectionContext(…​) so inject() resolves.

import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';

TestBed.configureTestingModule({
  providers: [provideRouter([{ path: 'products/:id', component: ProductPage }])],
});

const harness = await RouterTestingHarness.create();
const page = await harness.navigateByUrl('/products/7', ProductPage);
expect(page.id()).toBe(7);

// A functional guard in isolation:
const allowed = TestBed.runInInjectionContext(() => authGuard(routeSnapshot, stateSnapshot));
expect(allowed).toBe(true);

Component harnesses

@angular/cdk/testing harnesses give a stable, implementation-independent API for driving a component in tests. Load them through a HarnessLoader from TestbedHarnessEnvironment; Angular Material ships a harness per component.

import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';

const loader: HarnessLoader = TestbedHarnessEnvironment.loader(fixture);
const button = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await button.click();

Coverage and the Vitest migration

Run ng test --code-coverage to emit an Istanbul report under coverage/; enforce thresholds in karma.conf.js (coverageReporter.check). Treat coverage as a gap finder, not a target. See Code coverage.

Angular is moving the default runner from Karma/Jasmine (now deprecated) to Vitest. New workspaces can opt in with the @angular/build:unit-test builder; existing ones follow the Vitest migration guide. TestBed, ComponentFixture, and component harnesses are unchanged — only the runner and its assertion globals differ (vi in place of jasmine, Jest-style matchers).