Jest for Angular — Faster Unit Tests with jest-preset-angular

Jest is a Node.js-based test runner that executes tests without a browser — using jsdom to simulate the DOM. For Angular, jest-preset-angular handles TypeScript compilation, Angular-specific transforms, and TestBed compatibility. The result is significantly faster tests: a typical BlogApp test suite that takes 60 seconds in Karma completes in 15-20 seconds with Jest, with instant file-level re-runs in watch mode.

Migrating to Jest

// ── Installation ──────────────────────────────────────────────────────────
// npm install --save-dev jest jest-preset-angular @types/jest
// npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine
//              karma-jasmine-html-reporter @types/jasmine

// ── jest.config.ts ────────────────────────────────────────────────────────
import type { Config } from 'jest';

const config: Config = {
  preset:               'jest-preset-angular',
  setupFilesAfterFramework: ['<rootDir>/setup-jest.ts'],
  testEnvironment:      'jsdom',
  transform: {
    '^.+\\.(ts|mjs|js|html)$': [
      'jest-preset-angular',
      {
        tsconfig: '<rootDir>/tsconfig.spec.json',
        stringifyContentPathRegex: /\.html$/,
      },
    ],
  },
  moduleNameMapper: {
    // Map Angular path aliases
    '^@core/(.*)$':   '<rootDir>/src/app/core/$1',
    '^@shared/(.*)$': '<rootDir>/src/app/shared/$1',
    '^@features/(.*)$': '<rootDir>/src/app/features/$1',
  },
  collectCoverageFrom: [
    'src/app/**/*.ts',
    '!src/app/**/*.module.ts',
    '!src/environments/**',
  ],
  coverageThresholds: {
    global: {
      statements: 80,
      branches:   70,
      functions:  85,
      lines:      80,
    },
  },
  testMatch: ['**/*.spec.ts'],
};

export default config;

// ── setup-jest.ts ─────────────────────────────────────────────────────────
import 'jest-preset-angular/setup-jest';
// Add any global test setup here (e.g., custom matchers)

// ── tsconfig.spec.json — switch from jasmine to jest types ────────────────
// {
//   "extends": "./tsconfig.json",
//   "compilerOptions": {
//     "types": ["jest"],   // was ["jasmine"]
//     ...
//   }
// }

// ── package.json — update test scripts ────────────────────────────────────
// "scripts": {
//   "test":          "jest",
//   "test:watch":    "jest --watch",
//   "test:coverage": "jest --coverage"
// }

Jasmine → Jest Syntax Differences

// Most syntax is identical — describe, it, beforeEach, afterEach, expect
// Key differences:

// JASMINE: createSpy / createSpyObj
const spy = jasmine.createSpy('methodName');
const obj = jasmine.createSpyObj('ServiceName', ['method1', 'method2']);

// JEST equivalent:
const spy = jest.fn();
const obj = {
  method1: jest.fn(),
  method2: jest.fn(),
};

// JASMINE: spy.and.returnValue()
spy.and.returnValue(value);

// JEST equivalent:
spy.mockReturnValue(value);
(spy as jest.Mock).mockReturnValue(value);

// JASMINE: spy.and.callFake()
spy.and.callFake(() => value);

// JEST equivalent:
spy.mockImplementation(() => value);

// JASMINE: toHaveBeenCalledWith
expect(spy).toHaveBeenCalledWith(arg1, arg2);  // same in Jest ✓

// JASMINE: jasmine.objectContaining
expect(obj).toEqual(jasmine.objectContaining({ key: value }));

// JEST equivalent:
expect(obj).toEqual(expect.objectContaining({ key: value }));

// JEST-only: snapshot testing
it('should match post card snapshot', () => {
  const { container } = render(PostCardComponent, { componentInputs: { post } });
  expect(container.firstChild).toMatchSnapshot();
  // Creates/compares a stored snapshot of the rendered HTML
});
Note: Jest runs tests in parallel across multiple workers by default — one worker per CPU core. This is another reason Jest is faster than Karma: Karma runs all tests in a single browser instance sequentially (within the browser), while Jest distributes test files across workers simultaneously. A 50-file test suite might run in parallel across 4 workers, processing 12-13 files each rather than 50 sequentially. The exact speedup depends on test file count and complexity.
Tip: Jest’s --watch mode with intelligent test selection is the killer feature for TDD. When you save a source file, Jest only re-runs the test files that import the changed module — not the entire test suite. For a large BlogApp with 50 spec files, changing posts-api.service.ts only re-runs posts-api.service.spec.ts and any other spec that imports the service. This makes the TDD feedback loop nearly instant — sub-1-second from save to test result.
Warning: Some Angular-specific features require extra Jest configuration: Angular Material components may need BrowserAnimationsModule or NoopAnimationsModule, and Angular CDK’s OverlayContainer may need manual cleanup between tests. Test with your full component library after migration — not just simple component tests. If a subset of tests consistently fails after the Jest migration, check whether they depend on browser-specific APIs (window.matchMedia, IntersectionObserver) that jsdom doesn’t implement and need to be mocked globally in setup-jest.ts.

Common Mistakes

Mistake 1 — Not updating tsconfig.spec.json types (jasmine vs jest)

❌ Wrong — types: ["jasmine"] after migrating to Jest; TypeScript errors on jest-specific matchers (toMatchSnapshot, jest.fn()).

✅ Correct — types: ["jest"] in tsconfig.spec.json; removes jasmine types; enables jest types.

Mistake 2 — Using jest.fn() where a typed spy is needed (no IntelliSense)

❌ Wrong — const service = { getPublished: jest.fn() }; no type safety; wrong argument types not caught.

✅ Correct — const service = { getPublished: jest.fn<ReturnType<PostsApiService['getPublished']>>() }; preserves type safety.

🧠 Test Yourself

A test spec imports jasmine.createSpyObj() after migrating to Jest. TypeScript types are updated to Jest. What happens at test runtime?