In Angular, what is a CDK component harness, and how do you load one for a component in a TestBed unit test?
answer
- a typed API over a component
- a static selector for its host
- an environment builds the loader
- everything returns a promise
basics
~10 sA CDK component harness is a ComponentHarness subclass that drives a component through a typed async API; in a TestBed test you create the fixture, call TestbedHarnessEnvironment.loader(fixture), then await loader.getHarness(DropdownHarness).
solid answer
~40 sA component harness, from `@angular/cdk/testing`, is a class extending `ComponentHarness` with a static `hostSelector` that matches the component's selector; it exposes user-level methods like `selectOption()` and hides the markup behind them. In a unit test I create the fixture with `TestBed.createComponent`, build a `HarnessLoader` with `TestbedHarnessEnvironment.loader(fixture)` from `@angular/cdk/testing/testbed`, and `await loader.getHarness(DropdownHarness)`. Every harness method returns a `Promise`, and the harness runs change detection before reads and after actions. For the fixture's own root component I use `harnessForFixture`, and for overlays attached to the body `documentRootLoader`. The payoff is that only the harness knows the DOM, so markup refactors change one file, not every test.
code
ts · 27 linesimport { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { Dropdown } from './dropdown';
import { DropdownHarness } from './dropdown-harness';
@Component({
imports: [Dropdown],
template: `<app-dropdown [options]="countries" [(selected)]="country" />`,
})
class ProfileForm {
readonly countries = ['Norway', 'Chile', 'Japan'];
readonly country = signal<string | null>(null);
}
describe('ProfileForm country picker', () => {
it('stores the chosen country', async () => {
const fixture = TestBed.createComponent(ProfileForm);
const loader = TestbedHarnessEnvironment.loader(fixture);
const dropdown = await loader.getHarness(DropdownHarness);
await dropdown.selectOption('Chile');
expect(await dropdown.getSelectedLabel()).toBe('Chile');
expect(fixture.componentInstance.country()).toBe('Chile');
});
});go deeper
Recall the pieces: ComponentHarness with hostSelector, TestbedHarnessEnvironment.loader(fixture), await loader.getHarness(...), and await on every harness call.
Explain what the harness system does automatically, change detection and fresh element lookups, and when to use documentRootLoader or harnessForFixture instead of loader.
Show when a harness earns its cost, shared interactive components used widely, and how it confines markup knowledge to one file owned by the component's authors.
Discuss making harnesses part of a component library's public contract, versioned and reviewed with the components, so consumer test suites survive library upgrades.
## What a component harness is A **component harness** is a class that lets a test drive a component through a **supported, typed API** (`selectOption('Norway')`, `getSelectedLabel()`) instead of through the component's markup. The API lives in the **Angular CDK** (`@angular/cdk`), in the `@angular/cdk/testing` entry point, and every harness extends the base class **`ComponentHarness`**. - Each harness declares a static **`hostSelector`**, normally the component's own selector (for example `'app-dropdown'`). That is how the harness system finds instances of the component in the DOM. - Harness methods are **asynchronous** and return `Promise`s, so the same harness can run in unit tests and in WebDriver-based end-to-end tests. - Inside, the harness talks to the DOM only through **`TestElement`**, an environment-neutral wrapper with methods such as `click()`, `text()`, `sendKeys()` and `getAttribute()`. Shared components are the main use case: the team that owns a custom dropdown ships a `DropdownHarness`, and every feature that uses the dropdown tests through it. ## Loading a harness in a TestBed test Unit tests reach harnesses through a **`HarnessLoader`**, created by **`TestbedHarnessEnvironment`** from `@angular/cdk/testing/testbed`: 1. Create the component under test with `TestBed.createComponent(ProfileForm)`. 2. Build a loader with `TestbedHarnessEnvironment.loader(fixture)`. It is rooted at the fixture's root element and searches **below** it. 3. Ask it for a harness: `await loader.getHarness(DropdownHarness)`. 4. Call harness methods with `await` and assert on what they return. | Loader factory | Rooted at | Use it for | |---|---|---| | `TestbedHarnessEnvironment.loader(fixture)` | the fixture's root element | components rendered inside the tested component | | `TestbedHarnessEnvironment.documentRootLoader(fixture)` | the document root | overlays and pop-ups appended to `document.body` | | `TestbedHarnessEnvironment.harnessForFixture(fixture, Harness)` | returns a harness directly | the fixture's **own** root component | The last row matters: the component you created with `createComponent` is the fixture's root, and its host element does not carry the selector the harness looks for, so `loader.getHarness` cannot find it. `harnessForFixture` wraps the root element directly. ## What the harness does for you - **Change detection**: by default the harness system runs change detection before reading state and after every interaction, and waits for the fixture to become stable. Tests rarely call `fixture.detectChanges()` around harness calls. - **Stable queries**: element lookups happen at call time, so the harness never holds on to an element that a later render replaced. - **One vocabulary**: test authors learn `selectOption`, not the dropdown's internal class names. ## Why tests built on it survive refactors A test that reaches into a dropdown's markup (a CSS class on its trigger, the position of its option list) breaks when the dropdown's author restructures that markup, even though nothing a user can do has changed. With a harness, the **only** place that knows the markup is the harness, maintained by the same people who change the component. When the markup changes, one harness file changes and every consuming test keeps passing. That does not make harnesses free. They are worth writing for **shared, interactive components** used in many places; a one-off page component whose tests change together with its template gains little from one. ## Harness versus direct DOM access | Concern | Direct DOM queries in the test | Through a harness | |---|---|---| | Who knows the markup | every test that touches the component | only the harness | | Change detection | the test calls it at the right moments | handled around each harness call by default | | Stale elements | possible if a reference is kept across renders | lookups happen at call time | | Reuse in end-to-end tests | rewritten for the browser tool | same harness, different environment | | Cost | none up front | a harness class to write and maintain | Direct queries are covered by the fixture topic; what matters here is that the harness moves the markup knowledge out of the test. ## Common mistakes - Forgetting `await`, so a test asserts on a `Promise` instead of a value. - Using `loader.getHarness` for the fixture's own root component instead of `harnessForFixture`. - Mixing harness calls with direct DOM queries on the same component, which reintroduces the coupling the harness removed. - Installing nothing: harnesses need the `@angular/cdk` package in the project.
- Why can loader.getHarness(DropdownHarness) not find the dropdown when the dropdown itself is the component you passed to createComponent?The loader searches below the fixture's root element, and a component created as the fixture root does not carry its selector on that host element. `TestbedHarnessEnvironment.harnessForFixture(fixture, DropdownHarness)` builds the harness directly on the root element instead. Testing through a small host component that renders `<app-dropdown>` is the other common fix.
- Can the same harness be used outside TestBed?Yes, which is part of the design. Harnesses touch the DOM only through `TestElement`, so any harness environment can host them. The CDK ships the TestBed environment for unit tests and a WebDriver-based environment for end-to-end tests, and other environments can be added by implementing a harness environment.
saying these in an interview costs you the question
- A harness is Angular's name for the ComponentFixture returned by TestBed
- Harness methods are synchronous, so await is optional
- loader.getHarness finds the fixture's own root component
- You must call fixture.detectChanges() after every harness click
- Harnesses are part of @angular/core/testing and need no extra package