The Angular CDK provides code for creating component test harnesses. A component harness is a class that lets a test interact with a component via a supported API. Each harness’s API interacts with a component the same way a user would.
Component harnesses are based on the PageObject pattern commonly used for integration testing.
Benefits of Component Harnesses
Easier to Read Harnesses make tests easier to understand with straightforward APIs
More Robust Tests are less likely to break when updating Angular Material
Why Use Harnesses?
Abstraction - Tests don’t depend on component internals like DOM structure
Asynchronicity - Harnesses normalize async behavior automatically
Maintainability - Angular team updates harnesses, not your tests
Readability - Clear, self-documenting test code
Supported Test Environments
Karma (Unit Tests)
Selenium WebDriver (E2E)
Custom
Uses TestbedHarnessEnvironment for component testing with Angular TestBed
Uses SeleniumWebDriverHarnessEnvironment for end-to-end tests
Extend HarnessEnvironment and TestElement for other environments
Getting Started
Import the harness environment
import { HarnessLoader } from '@angular/cdk/testing' ;
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' ;
let loader : HarnessLoader ;
describe ( 'my-component' , () => {
beforeEach ( async () => {
TestBed . configureTestingModule ({
imports: [ MyModule ],
declarations: [ UserProfile ]
});
fixture = TestBed . createComponent ( UserProfile );
loader = TestbedHarnessEnvironment . loader ( fixture );
});
});
@angular/cdk/testing - Shared symbols for all environments
@angular/cdk/testing/testbed - Karma-specific symbols
@angular/cdk/testing/selenium-webdriver - Selenium-specific symbols
Load a harness
import { MatButtonHarness } from '@angular/material/button/testing' ;
it ( 'should work' , async () => {
const buttons = await loader . getAllHarnesses ( MatButtonHarness );
const firstButton = await loader . getHarness ( MatButtonHarness );
});
All harness APIs are asynchronous and return Promise objects. Use async/await syntax.
Filter harnesses
Use with() to filter harnesses by specific criteria: it ( 'should work' , async () => {
// By id
const info = await loader . getHarness (
MatButtonHarness . with ({ selector: '#more-info' })
);
// By text
const cancel = await loader . getHarness (
MatButtonHarness . with ({ text: 'Cancel' })
);
// By multiple criteria
const okButton = await loader . getHarness (
MatButtonHarness . with ({
selector: '.confirm' ,
text: / ^ ( Ok | Okay ) $ /
})
);
});
Interact with components
it ( 'should mark confirmed when ok button clicked' , async () => {
const okButton = await loader . getHarness (
MatButtonHarness . with ({ selector: '.confirm' })
);
expect ( fixture . componentInstance . confirmed ). toBe ( false );
expect ( await okButton . isDisabled ()). toBe ( false );
await okButton . click ();
expect ( fixture . componentInstance . confirmed ). toBe ( true );
});
Harnesses automatically call change detection and wait for stability. No need for detectChanges() or whenStable()!
Loading Harnesses from Specific Sections
You can load harnesses for a sub-section of the DOM:
it ( 'should work' , async () => {
const footerLoader = await loader . getChildLoader ( '.footer' );
const footerButton = await footerLoader . getHarness ( MatButtonHarness );
});
Filter Options
All harnesses support these standard options:
CSS selector that the component must match
CSS selector for an ancestor element
Additional options vary by component. For example, MatButtonHarness also supports:
Text content or regex pattern to match
Comparison: With vs Without Harnesses
Without Harnesses
With Harnesses
it ( 'should switch to bug report template' , async () => {
expect ( fixture . debugElement . query ( 'bug-report-form' )). toBeNull ();
const selectTrigger = fixture . debugElement . query (
By . css ( '.mat-select-trigger' )
);
selectTrigger . triggerEventHandler ( 'click' , {});
fixture . detectChanges ();
await fixture . whenStable ();
const options = document . querySelectorAll ( '.mat-select-panel mat-option' );
options [ 1 ]. click (); // Click the second option, "Bug"
fixture . detectChanges ();
await fixture . whenStable ();
expect ( fixture . debugElement . query ( 'bug-report-form' )). not . toBeNull ();
});
This test depends on internal DOM structure and requires manual change detection.
it ( 'should switch to bug report template' , async () => {
expect ( fixture . debugElement . query ( 'bug-report-form' )). toBeNull ();
const select = await loader . getHarness ( MatSelectHarness );
await select . open ();
const bugOption = await select . getOption ({ text: 'Bug' });
await bugOption . click ();
expect ( fixture . debugElement . query ( 'bug-report-form' )). not . toBeNull ();
});
Clear, self-documenting code that doesn’t depend on component internals.
Key Advantages
await select.open() is self-explanatory
Filter API makes code self-documenting
No comments needed to explain intent
Eliminates repetitive detectChanges() and whenStable() calls
No dependency on internal CSS selectors
Protected from DOM restructuring
Normalized asynchronous behavior
Angular team maintains harness compatibility
Survives component refactoring
Works across async/sync changes
One update point for breaking changes
Reduces maintenance burden
Common Patterns
it ( 'should validate form' , async () => {
const input = await loader . getHarness (
MatInputHarness . with ({ selector: '[name="email"]' })
);
await input . setValue ( 'invalid-email' );
expect ( await input . hasError ( 'email' )). toBe ( true );
await input . setValue ( '[email protected] ' );
expect ( await input . hasError ()). toBe ( false );
});
Testing Dialogs
it ( 'should open dialog' , async () => {
const button = await loader . getHarness (
MatButtonHarness . with ({ text: 'Open Dialog' })
);
await button . click ();
const dialog = await documentRootLoader . getHarness ( MatDialogHarness );
expect ( await dialog . getTitleText ()). toBe ( 'Confirmation' );
await dialog . close ();
});
it ( 'should select menu item' , async () => {
const menuTrigger = await loader . getHarness ( MatMenuHarness );
await menuTrigger . open ();
const items = await menuTrigger . getItems ();
await items [ 0 ]. click ();
expect ( component . selectedAction ). toBe ( 'action-1' );
});
Available Harnesses
Angular Material provides harnesses for most components:
Form Controls Input, Select, Checkbox, Radio, Slider, Datepicker
Buttons & Indicators Button, Icon Button, FAB, Chips, Badge
Navigation Menu, Tabs, Stepper, Tree
Layout Card, List, Grid List, Divider
Popups Dialog, Bottom Sheet, Snackbar, Tooltip
Data Table, Sort, Paginator, Tree
Best Practices
Always use harnesses when available
If a harness exists for a component, use it instead of querying DOM directly.
Filter by semantic properties
Use text content, labels, or roles rather than CSS classes or ids when possible.
Use child loaders for specificity
When multiple instances exist, use getChildLoader() to scope searches.
Leverage TypeScript
Harnesses are fully typed - let your IDE help you discover available methods.
Resources
CDK Testing Official CDK testing documentation
API Reference Complete harness API reference
Testing Guide Testing strategies and best practices
Custom Harnesses Create your own component harnesses