Snapshot Testing Angular Components with Jest

Snapshot Testing Angular Components with Jest

Angular's default test runner is Jasmine with Karma. Many teams switch to Jest for faster test runs, better snapshot support, and a more modern testing experience. Once Jest is configured, snapshot testing Angular components works much like React — with a few Angular-specific considerations.

This guide covers setting up Jest for Angular, writing component snapshots, and handling Angular-specific patterns.

Setup: Jest for Angular

Installation

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

Configuration

// jest.config.js
module.exports = {
  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|svg)$'
      }
    ]
  },
  moduleNameMapper: {
    '@app/(.*)': '<rootDir>/src/app/$1'
  }
}
// setup-jest.ts
import 'jest-preset-angular/setup-jest'
// tsconfig.spec.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": ["jest"]
  },
  "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}

Writing Component Snapshots

Basic Snapshot

// user-card.component.ts
@Component({
  selector: 'app-user-card',
  template: `
    <div class="user-card">
      <img [src]="avatar" [alt]="name">
      <h2>{{ name }}</h2>
      <p>{{ role }}</p>
    </div>
  `
})
export class UserCardComponent {
  @Input() name: string
  @Input() role: string
  @Input() avatar: string
}
// user-card.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { UserCardComponent } from './user-card.component'

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [UserCardComponent]
    }).compileComponents()

    fixture = TestBed.createComponent(UserCardComponent)
    component = fixture.componentInstance
  })

  it('renders with props', () => {
    component.name = 'Alice'
    component.role = 'Senior Engineer'
    component.avatar = 'https://example.com/alice.jpg'
    
    fixture.detectChanges()
    
    expect(fixture.nativeElement).toMatchSnapshot()
  })
})

Standalone Components (Angular 14+)

// Angular 14+ standalone component
@Component({
  standalone: true,
  selector: 'app-badge',
  template: `<span class="badge" [class]="variant">{{ label }}</span>`,
  imports: [CommonModule]
})
export class BadgeComponent {
  @Input() label: string
  @Input() variant: 'success' | 'warning' | 'error' = 'success'
}

// Test
it('renders success badge', async () => {
  await TestBed.configureTestingModule({
    imports: [BadgeComponent]  // import instead of declare for standalone
  }).compileComponents()

  const fixture = TestBed.createComponent(BadgeComponent)
  fixture.componentInstance.label = 'Active'
  fixture.componentInstance.variant = 'success'
  fixture.detectChanges()

  expect(fixture.nativeElement).toMatchSnapshot()
})

Shallow vs Deep Rendering

Angular's TestBed renders child components by default (deep). For shallow rendering, declare child components as NO_ERRORS_SCHEMA or use stubs.

import { NO_ERRORS_SCHEMA } from '@angular/core'

beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [ParentComponent],
    schemas: [NO_ERRORS_SCHEMA]  // suppresses unknown element errors for child components
  }).compileComponents()
})

With NO_ERRORS_SCHEMA, child component selectors are rendered as unknown elements in the snapshot:

<app-parent>
  <app-child-component></app-child-component>  <!-- not fully rendered -->
</app-parent>

This is the Angular equivalent of React's shallow() rendering.

Stub Child Components

For more explicit control, declare stub components:

@Component({ selector: 'app-header', template: '<div>HEADER STUB</div>' })
class MockHeaderComponent {}

@Component({ selector: 'app-footer', template: '<div>FOOTER STUB</div>' })
class MockFooterComponent {}

beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [AppComponent, MockHeaderComponent, MockFooterComponent]
  }).compileComponents()
})

Stubs give you explicit control over stub output, which makes snapshot diffs easier to read.

Testing Component States

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ButtonComponent]
    }).compileComponents()

    fixture = TestBed.createComponent(ButtonComponent)
    component = fixture.componentInstance
  })

  it('default state', () => {
    component.label = 'Submit'
    fixture.detectChanges()
    expect(fixture.nativeElement).toMatchSnapshot()
  })

  it('loading state', () => {
    component.label = 'Submit'
    component.loading = true
    fixture.detectChanges()
    expect(fixture.nativeElement).toMatchSnapshot()
  })

  it('disabled state', () => {
    component.label = 'Submit'
    component.disabled = true
    fixture.detectChanges()
    expect(fixture.nativeElement).toMatchSnapshot()
  })
})

Async Components and ngOnInit

For components that fetch data in ngOnInit:

import { of } from 'rxjs'
import { UserService } from './user.service'

it('renders after data loads', async () => {
  const mockUserService = {
    getUser: jest.fn().mockReturnValue(of({ name: 'Alice', email: 'alice@example.com' }))
  }

  await TestBed.configureTestingModule({
    declarations: [UserProfileComponent],
    providers: [
      { provide: UserService, useValue: mockUserService }
    ]
  }).compileComponents()

  const fixture = TestBed.createComponent(UserProfileComponent)
  fixture.detectChanges()  // triggers ngOnInit
  
  await fixture.whenStable()  // wait for async operations
  fixture.detectChanges()     // update view after data arrives
  
  expect(fixture.nativeElement).toMatchSnapshot()
})

Handling Dynamic Data

Freeze Dates

import MockDate from 'mockdate'

beforeEach(() => MockDate.set('2024-01-15T12:00:00Z'))
afterEach(() => MockDate.reset())

it('shows creation date', () => {
  component.item = { title: 'Test', createdAt: new Date() }
  fixture.detectChanges()
  expect(fixture.nativeElement).toMatchSnapshot()
})

Mock Random/Generated Values

If the component generates IDs or uses Math.random():

beforeEach(() => {
  jest.spyOn(Math, 'random').mockReturnValue(0.5)
})

afterEach(() => {
  jest.restoreAllMocks()
})

Snapshot the nativeElement vs innerHTML

Both approaches work, with different output:

// Option 1: nativeElement — includes the component host element
expect(fixture.nativeElement).toMatchSnapshot()
// Output: <app-user-card><div class="user-card">...</div></app-user-card>

// Option 2: innerHTML — just the component's template content
expect(fixture.nativeElement.innerHTML).toMatchSnapshot()
// Output: <div class="user-card">...</div>

// Option 3: specific element
const card = fixture.nativeElement.querySelector('.user-card')
expect(card).toMatchSnapshot()

innerHTML is often cleaner for component library testing — you don't see the host element wrapper.

Inline Snapshots for Small Components

it('renders badge', () => {
  component.label = 'New'
  component.variant = 'success'
  fixture.detectChanges()

  expect(fixture.nativeElement.innerHTML.trim()).toMatchInlineSnapshot(`
    "<span class="badge badge--success">New</span>"
  `)
})

Updating Snapshots

# Update all snapshots
npx jest --updateSnapshot

# Update specific test file
npx jest user-card.component.spec.ts --updateSnapshot

# Interactive mode — press u to update
npx jest --watch

Common Issues

Zone.js and async: Angular uses Zone.js for change detection. In tests, always call fixture.detectChanges() after changing inputs, and await fixture.whenStable() after async operations.

Template compilation: If using jest-preset-angular, HTML templates are compiled as part of the Jest transform. If templates aren't resolving, check the transform config covers .html files.

Change detection: Angular's default change detection is Default (automatic). If your component uses OnPush, you need to manually mark it as dirty:

// For OnPush components
fixture.changeDetectorRef.markForCheck()
fixture.detectChanges()

Summary

Snapshot testing Angular components with Jest requires:

  1. jest-preset-angular for Angular template support
  2. TestBed.configureTestingModule for each test
  3. fixture.detectChanges() to trigger rendering
  4. NO_ERRORS_SCHEMA or component stubs for shallow rendering

Once set up, the snapshot API is identical to React — toMatchSnapshot(), toMatchInlineSnapshot(), and --updateSnapshot work the same way. The Angular-specific part is the setup and the change detection lifecycle.

Read more

Start now free