Skip to content
Angular ng testing 4 min read

Component Test Harnesses

A test that reaches into a component’s DOM with raw CSS selectors is a test coupled to that component’s internal markup. Rename a class, restructure a wrapper <div>, or upgrade Angular Material, and the test breaks even though the behaviour is unchanged. Component test harnesses, part of the Angular CDK, solve this by exposing a stable, semantic API for interacting with a component — you ask a button harness to “click” or a form-field harness for “the error text” instead of querying .mat-mdc-form-field-error. Tests written against harnesses survive refactors and even render-engine swaps.

What a harness is

A harness is a class that wraps a component instance in the DOM and offers async methods describing what the component does, not how it is built. Angular Material and the CDK ship harnesses for every component (MatButtonHarness, MatInputHarness, MatSelectHarness, and so on), and you can author your own for first-party components. Every harness method returns a Promise, because harnesses are designed to work identically in a unit test (Karma/Jasmine or Vitest) and in an end-to-end environment (Protractor/WebDriver), where DOM access is inherently asynchronous.

You obtain harnesses through a HarnessLoader, which you get from TestbedHarnessEnvironment in a unit test. The loader’s job is to find matching harness instances within a fixture’s DOM.

Loading harnesses in a TestBed test

Install the harness packages alongside the CDK:

npm install @angular/cdk @angular/material

Set up the loader once per test, then query it. Because harness methods are async, your test bodies become async and use await:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatInputHarness } from '@angular/material/input/testing';
import { LoginFormComponent } from './login-form.component';

describe('LoginFormComponent', () => {
  let fixture: ComponentFixture<LoginFormComponent>;
  let loader: HarnessLoader;

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

    fixture = TestBed.createComponent(LoginFormComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
    fixture.detectChanges();
  });

  it('submits when the form is filled', async () => {
    const email = await loader.getHarness(
      MatInputHarness.with({ selector: '[formControlName="email"]' }),
    );
    await email.setValue('[email protected]');

    const submit = await loader.getHarness(
      MatButtonHarness.with({ text: 'Sign in' }),
    );
    expect(await submit.isDisabled()).toBe(false);
    await submit.click();

    expect(fixture.componentInstance.submitted()).toBe(true);
  });
});

Notice there is no fixture.detectChanges() between the harness calls. Harnesses run a stabilization step automatically after each interaction, flushing change detection and the microtask queue so the DOM reflects the latest state before the next assertion.

Prefer loader.getHarness() for the common case — it throws a descriptive error if zero or multiple matches are found, catching ambiguous queries early. Use getAllHarnesses() only when you genuinely expect a list.

Finding the right harness

Harnesses are located with predicates. The static with(...) method narrows a query by text, selector, ancestor, or harness-specific options, which keeps your test stable when several similar components exist on the page.

MethodReturnsBehaviour when no match
getHarness(predicate)single harnessthrows
getHarnessOrNull(predicate)harness or nullreturns null
getAllHarnesses(predicate)array of harnessesreturns []
hasHarness(predicate)booleanreturns false

For deeply nested layouts, narrow the loader itself to a sub-tree before querying, so ambiguous selectors only ever resolve within the region you care about:

const dialogLoader = await loader.getChildLoader('.app-settings-dialog');
const saveButton = await dialogLoader.getHarness(
  MatButtonHarness.with({ text: 'Save' }),
);
await saveButton.click();

Authoring a custom harness

For your own components, extend ComponentHarness, declare a hostSelector, and expose async methods built on the protected locatorFor helpers. Suppose you have a standalone rating widget:

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

@Component({
  selector: 'app-star-rating',
  standalone: true,
  template: `
    <button
      *ngFor="let i of stars"
      class="star"
      [class.filled]="i <= value()"
      (click)="value.set(i)">★</button>
  `,
})
export class StarRatingComponent {
  max = input(5);
  value = signal(0);
  get stars() {
    return Array.from({ length: this.max() }, (_, i) => i + 1);
  }
}

The harness translates DOM details into intent-revealing operations:

import { ComponentHarness } from '@angular/cdk/testing';

export class StarRatingHarness extends ComponentHarness {
  static hostSelector = 'app-star-rating';

  private stars = this.locatorForAll('.star');

  async getValue(): Promise<number> {
    const stars = await this.stars();
    let count = 0;
    for (const star of stars) {
      if ((await star.getAttribute('class'))?.includes('filled')) count++;
    }
    return count;
  }

  async rate(stars: number): Promise<void> {
    const buttons = await this.stars();
    await buttons[stars - 1].click();
  }
}

Consuming it reads like a description of user behaviour:

it('records a three-star rating', async () => {
  const rating = await loader.getHarness(StarRatingHarness);
  await rating.rate(3);
  expect(await rating.getValue()).toBe(3);
});

Output:

LoginFormComponent
  ✓ submits when the form is filled
StarRatingComponent
  ✓ records a three-star rating

Tests: 2 passed, 2 total

Best practices

  • Reach for harnesses whenever you test components that ship one (all of Angular Material and the CDK) — they decouple tests from internal markup and CSS class names.
  • Always await harness calls; a missing await leaves you asserting on a Promise and produces confusing green-then-flaky tests.
  • Let the harness handle stabilization. Avoid manual fixture.detectChanges() or tick() around harness interactions unless you are mixing in non-harness code.
  • Narrow queries with .with({ ... }) or getChildLoader instead of broad selectors so tests stay unambiguous as the page grows.
  • Author custom harnesses for your design-system components and reuse them across the suite — the harness becomes the single source of truth for how a component is operated.
  • Expose only semantic intent (rate(3), getErrorText()) from custom harnesses; never leak raw CSS selectors to the test that consumes them.
Last updated June 14, 2026
Was this helpful?