Skip to content
Angular ng libraries 4 min read

Jest

Jest is a fast, batteries-included JavaScript test runner that has become the de facto choice for unit testing modern Angular applications. Unlike the legacy Karma + Jasmine setup that spins up a real browser, Jest runs your tests in a simulated DOM (jsdom) directly in Node, which makes the feedback loop dramatically quicker and the configuration far simpler. With Angular’s official @angular/build:unit-test builder now supporting Jest, plus the mature community preset jest-preset-angular, wiring Jest into an Angular workspace is a short, well-trodden path.

Why Jest over Karma

Karma was officially deprecated by the Angular team, and new workspaces no longer scaffold it by default. Jest fills that gap with a richer feature set and a noticeably better developer experience.

ConcernKarma + JasmineJest
RuntimeReal browserjsdom (Node)
StartupLaunches Chrome each runNo browser, instant boot
Watch modeReruns everythingReruns only affected tests
ParallelismSingle browser contextMulti-worker by default
SnapshotsNot built inFirst-class
MockingManual / Jasmine spiesPowerful auto-mocking + jest.fn()
CoverageKarma coverage pluginBuilt-in (--coverage)

The main trade-off: jsdom is not a real browser, so layout, pixel measurements, and certain native APIs are stubbed. For genuine browser behavior reach for end-to-end tools like Playwright or Cypress instead.

Option 1: the Angular Jest builder (experimental)

Angular 20+ ships an experimental unit-test builder that can use Jest under the hood, so you do not need a third-party preset. Configure it in angular.json:

{
  "projects": {
    "app": {
      "architect": {
        "test": {
          "builder": "@angular/build:unit-test",
          "options": {
            "tsConfig": "tsconfig.spec.json",
            "runner": "jest",
            "buildTarget": "::development"
          }
        }
      }
    }
  }
}

Then run it through the Angular CLI:

ng test

Because this builder is still experimental, most production projects today still rely on jest-preset-angular, covered next.

Option 2: jest-preset-angular

This is the battle-tested community approach. Install the dependencies:

npm install --save-dev jest jest-preset-angular @types/jest

Create jest.config.ts at the project root:

import type { Config } from 'jest';

const config: Config = {
  preset: 'jest-preset-angular',
  setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
  testEnvironment: 'jsdom',
  collectCoverageFrom: ['src/app/**/*.ts', '!src/app/**/*.spec.ts'],
};

export default config;

Add setup-jest.ts to initialise the Angular testing environment once per run:

import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';

setupZoneTestEnv();

If your app is zoneless (Angular 18+ with provideExperimentalZonelessChangeDetection()), import from jest-preset-angular/setup-env/zoneless instead so the harness matches your change-detection strategy.

Finally, wire the scripts in package.json:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage"
  }
}

Writing a test for a standalone component

Jest uses the same Angular TestBed API you already know, so existing specs need little to no change. Here is a modern standalone component using signals and the new control flow:

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    @if (count() > 0) {
      <span class="positive">positive</span>
    }
    <button (click)="increment()">+</button>
  `,
})
export class CounterComponent {
  readonly count = signal(0);
  increment(): void {
    this.count.update((value) => value + 1);
  }
}

The spec drives it through TestBed:

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

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [CounterComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('starts at zero', () => {
    expect(component.count()).toBe(0);
  });

  it('renders the positive flag after a click', () => {
    component.increment();
    fixture.detectChanges();

    const el: HTMLElement = fixture.nativeElement;
    expect(el.querySelector('.positive')?.textContent).toBe('positive');
    expect(el.querySelector('p')?.textContent).toContain('Count: 1');
  });
});

Run it:

npm test

Output:

 PASS  src/app/counter/counter.component.spec.ts
  CounterComponent
    ✓ starts at zero (24 ms)
    ✓ renders the positive flag after a click (9 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.43 s

Mocking services with jest.fn()

Jest’s mocking is one of its biggest wins over Jasmine. Provide a fake using plain jest.fn() and assert on calls:

import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
import { ProfileComponent } from './profile.component';

const userServiceMock = {
  load: jest.fn().mockResolvedValue({ id: 1, name: 'Ada' }),
};

beforeEach(async () => {
  await TestBed.configureTestingModule({
    imports: [ProfileComponent],
    providers: [{ provide: UserService, useValue: userServiceMock }],
  }).compileComponents();
});

it('loads the user on init', async () => {
  const fixture = TestBed.createComponent(ProfileComponent);
  fixture.detectChanges();
  await fixture.whenStable();

  expect(userServiceMock.load).toHaveBeenCalledTimes(1);
});

Best Practices

  • Prefer jest-preset-angular for production projects today; reserve the Angular Jest builder for greenfield apps where experimental status is acceptable.
  • Match your setup file to your change-detection mode (zone vs zoneless) to avoid subtle async failures.
  • Call fixture.detectChanges() after mutating signals or inputs so the rendered DOM reflects the new state before you assert.
  • Use jest.fn() mocks and useValue providers instead of hitting real services, keeping unit tests fast and deterministic.
  • Run jest --watch during development so only the tests affected by your changes rerun.
  • Reserve snapshot tests for stable, presentational markup; overusing them on volatile templates creates brittle, noisy diffs.
  • Track coverage with jest --coverage in CI, but treat it as a guide rather than a goal in itself.
Last updated June 14, 2026
Was this helpful?