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. UsegetAllHarnesses()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.
| Method | Returns | Behaviour when no match |
|---|---|---|
getHarness(predicate) | single harness | throws |
getHarnessOrNull(predicate) | harness or null | returns null |
getAllHarnesses(predicate) | array of harnesses | returns [] |
hasHarness(predicate) | boolean | returns 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
awaitharness calls; a missingawaitleaves you asserting on aPromiseand produces confusing green-then-flaky tests. - Let the harness handle stabilization. Avoid manual
fixture.detectChanges()ortick()around harness interactions unless you are mixing in non-harness code. - Narrow queries with
.with({ ... })orgetChildLoaderinstead 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.