Skip to content

Angular Component Testing with Karma

Component tests verify that a template renders correctly and responds to user interaction and input changes. This lesson covers ComponentFixture, detectChanges(), and querying the DOM inside a real browser via Karma.

The ComponentFixture

TestBed.createComponent() returns a ComponentFixture, a wrapper giving you access to the component instance itself, its rendered DOM (nativeElement), and control over Angular's change detection cycle via detectChanges().

import { TestBed } from '@angular/core/testing';
import { GreetingComponent } from './greeting.component';

describe('GreetingComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [GreetingComponent]
    }).compileComponents();
  });

  it('renders the provided name', () => {
    const fixture = TestBed.createComponent(GreetingComponent);
    fixture.componentInstance.name = 'Ada';
    fixture.detectChanges();

    const el = fixture.nativeElement as HTMLElement;
    expect(el.querySelector('h1')?.textContent).toContain('Ada');
  });
});

fixture.detectChanges() triggers Angular to re-render the template based on the component's current state — without it, DOM assertions would see stale content.

The ComponentFixture API

fixture.componentInstance      // the actual component class instance
fixture.nativeElement            // the rendered root DOM element (real browser DOM via Karma)
fixture.detectChanges()           // triggers Angular's change detection / re-render
fixture.debugElement               // Angular's wrapped element, useful for querying by directive
  • nativeElement is a real DOM element because Karma runs the test in an actual browser — no jsdom approximation involved.
  • detectChanges() must be called after changing component state directly, or the template won't reflect it yet.
  • debugElement.query(By.css('selector')) is the Angular-idiomatic way to find elements, layering extra metadata over plain DOM queries.
  • Calling detectChanges() too rarely is one of the most common sources of 'my assertion sees stale content' failures.

Component Testing Cheat Sheet

The essentials for testing an Angular component's rendered output and behavior.

Task Approach
Create the fixture TestBed.createComponent(MyComponent)
Set an input directly fixture.componentInstance.someInput = value
Re-render after a change fixture.detectChanges()
Query rendered DOM fixture.nativeElement.querySelector(...)
Simulate a click element.dispatchEvent(new Event('click')) then detectChanges()
Query by Angular directive fixture.debugElement.query(By.directive(MyDirective))

Simulating User Interaction

Because Karma runs a real browser, you can dispatch genuine DOM events — clicks, input changes, keyboard events — exactly as a real user would trigger them, then call detectChanges() to let Angular process the resulting state change.

it('increments the counter on click', () => {
  const fixture = TestBed.createComponent(CounterComponent);
  fixture.detectChanges();

  const button = fixture.nativeElement.querySelector('button');
  button.click();
  fixture.detectChanges();

  const countEl = fixture.nativeElement.querySelector('.count');
  expect(countEl.textContent).toContain('1');
});

Real .click() on a real DOM element is available precisely because this test runs inside an actual browser via Karma.

Testing @Input() and @Output() Bindings

Component inputs can be set directly on componentInstance; outputs (EventEmitter) can be subscribed to directly in the test to assert they emitted the expected value.

it('emits selected id on row click', () => {
  const fixture = TestBed.createComponent(UserRowComponent);
  fixture.componentInstance.user = { id: 7, name: 'Ada' };

  let emittedId: number | undefined;
  fixture.componentInstance.selected.subscribe((id: number) => (emittedId = id));

  fixture.detectChanges();
  fixture.nativeElement.querySelector('tr').click();

  expect(emittedId).toBe(7);
});

Common Mistakes

  • Forgetting to call detectChanges() after changing component state, then asserting against stale rendered DOM.
  • Using plain DOM queries when debugElement's directive-aware queries would be clearer for component-specific lookups.
  • Not resetting a component's state between assertions within the same spec, leaking state across checks.
  • Testing implementation details of the component class instead of its actual rendered output and emitted events.

Key Takeaways

  • TestBed.createComponent() returns a ComponentFixture wrapping the component instance and its real rendered DOM.
  • detectChanges() must be called to reflect state changes in the rendered template.
  • Because Karma runs a real browser, genuine DOM events (.click(), etc.) can be dispatched directly in tests.
  • Inputs are set directly on the component instance; outputs are asserted by subscribing to the EventEmitter.

Pro Tip

If a component test behaves unexpectedly, add an extra fixture.detectChanges() call before your assertion as a quick diagnostic step. If that alone fixes it, you've found a missing change-detection trigger rather than a real bug in the component.