How and why to use Angular materials component harness to write reliable, stable and readable component tests

Kevin Kreuzer
@nivekcode
Nov 15, 2022
6 min read
Component harnesses represent a standout feature in Angular Material that can substantially enhance the quality of our component tests. Throughout this article, we explore how to leverage component harnesses for creating more robust and more legible tests.
This blog is also available as a YouTube video on my channel. Subscribe and never miss new Angular videos.
Testing in Angular
Reliable tests form the foundation of any well-built application. They confirm that our software behaves as intended. Additionally, a solid test suite makes refactoring safe. When tests are dependable, we can modify internal mechanisms without altering observable behavior.
Within Angular, we categorize tests into several types: unit tests, component tests (also referred to as integration tests), and end-to-end tests.
The testing pyramid for Angular applications
Unit tests are typically the simplest to author; they validate functions within our TypeScript code. Component tests and end-to-end tests, in contrast, pose greater challenges. These tests exercise not only the TypeScript logic but also the template, which frequently incorporates third-party libraries like Material.
What’s the problem with today's component tests? 🤔
When testing components that incorporate Angular Material, our standard approach involves using a CSS query selector.
To illustrate this point, let's examine several tests from a "Game of Thrones" filter demo application.
Game of Thrones characters filter tableThis application is quite straightforward; it lets you filter all characters based on their "alive status" through radio buttons. Furthermore, we can narrow down the characters by entering search text into the input field.
For instance, clicking the radio button labeled "Dead" filters the table accordingly.
filtered (dead) game of thrones characters
Let's create a unit test that verifies the radio button filter functionality. This test confirms that the appropriate method gets invoked and the data source is filtered as expected. We validate the interaction between our template, Material, and our TypeScript logic.
it('should filter out the alive caracters if we set filter to "dead"', (done) => {
const deadRadio = fixture.debugElement.query(By.css('#deadFilter'));
const clickableElement = deadRadio.query(By.css('.mat-radio-container'));
clickableElement.nativeElement.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
const rows = fixture.debugElement.queryAll(By.css('.mat-table tbody tr'));
expect(rows.length).toBe(5);
done();
});
});
Initially, we retrieve the radio button using the id deadFilter. Unfortunately, executing a click directly on the radio button yields no result. Consequently, we query the radio button element to locate the clickable target, which is the element carrying the mat-radio-container class.
After invoking the click method on that clickable element, we call fixture.detectChanges() to instruct the TestBed to perform data binding. Subsequently, we await the promise returned by fixture.whenStable(). By awaiting this promise, we ensure that the JavaScript engine's task queue has been drained.
At this stage, we can access the table rows and verify their count. Since we are using a then handler, we must invoke Jasmine's done callback to guarantee that the test completes only after our assertions have been evaluated.
This approach carries several drawbacks, some of which you may have already spotted. 😉
We rely on internal implementation details
To select a radio button, simply grabbing it and calling .click on its nativeElement isn't sufficient. We must locate the clickable element nested within the radio button structure.
const deadRadio = fixture.debugElement.query(By.css('#deadFilter'));
const clickableElement = deadRadio.query(By.css('.mat-radio-container'));
clickableElement.nativeElement.click();
Querying Material components using CSS selectors is problematic for several reasons;
First, we are required to comprehend the inner workings of Material components. To identify the clickable element, we have to dig into their internal structure.
Second, consider a scenario where Material modifies the internal DOM layout of the radio button or simply renames the mat-radio-container class to mat-radio-box. Our test would break, even though our application continues to function correctly, wouldn't it?
Relying on implementation details of third party libraries is cumbersome because you are vulnerable to refactorings and you need to understand implementation details
Manually stabilize the fixture
A fixture reaches stability once Angular has resolved all bindings and the JavaScript task queue is empty. To ensure a stabilized fixture, we must remember to invoke fixture.detectChanges and fixture.whenStable.
fixture.detectChanges();
fixture.whenStable().then(() => {});
Remember to call those statements is cumbersome. "If we forget it, we may end up in long and confusing debugging sessions" (burned child 😉).
Call done when you’re done
Once we begin asserting within an asynchronous callback (for instance, using Promise or Subscriptions), we need to ensure that the done function is called after our assertions. Neglecting this step means our assertions might never execute. Consequently, our test could pass even when it should actually fail.
Tipp: When we work with promises we can also work with
async/awaitstatements instead of callbacks. Then we don't have to call thedonefunction.
Follow me on Twitter because you will get notified about new TypeScript blog posts and cool frontend stuff!😉
Angular Material test harness to the rescue ⛑️
The "Harness concept" draws inspiration from the PageObject pattern. A harness class enables a test to interact with a component through a formal API.
By adopting component harnesses, a test shields itself from the internals of the component library and remains resilient to internal refactorings.
That sounds promising — how do we utilize Material's test harness?
let loader: HarnessLoader;
beforeEach(() => {
fixture = TestBed.createComponent(FilterTableComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
fixture.detectChanges();
});
The initial step involves obtaining a loader. We declare a variable in our top-level describe block and assign it within the beforeEach hook. We employ the TestbedHarnessEnvironment to acquire a HarnessLoader instance.
A
HarnessEnvironmentis the foundation of our harness test. There are different types of HarnessEnvironment's. For Karma/Jasmine environment we use theTestbedHarnessEnvironment.
That constitutes the entire setup. Now we can proceed to benefit from component harnesses in our test. Let's revisit and rewrite the filter test we examined earlier.
it('should filter out the alive caracters if we set filter to dead', async () => {
const deadRadioButton = await loader.getHarness<MatRadioButtonHarness>(
MatRadioButtonHarness.with({ label: 'Dead' }),
);
const table = await loader.getHarness<MatTableHarness>(MatTableHarness);
await deadRadioButton.check();
const rows = await table.getRows();
expect(rows.length).toBe(5);
});
Observe how straightforward and readable our test has become! 🤩
When writing harness tests, we frequently rely on async/await. Consequently, one of the initial habits we should adopt is placing the async keyword before our callback.
The loader enables us to load harness objects. In our scenario, we're interested in the harness for the radio button labeled "Dead". Once we have the harness, we can utilize its API to invoke the check method. The check method is self-explanatory and internally understands how to select the checkbox — we no longer need to investigate the implementation details of mat-radio-button.
The same principle applies to the table. Given that our page contains only one table, it suffices to request the MatTableHarness from the loader without specifying a selector. After obtaining the MatTableharness, we can use the getRows function to determine the row count.
Notably, we didn't call fixture.detectChanges() or fixture.whenStable(). The Material component harness automatically stabilizes the fixture when we interact with a component. This is a remarkable advantage that makes our tests less susceptible to errors.
Another significant benefit is the enhanced readability of our tests. Consider the following two examples — what are your thoughts? Which tests are simpler to comprehend? The traditional tests or the harness tests? Which version contains less boilerplate?
Classic component test and harness component test which test the GOT character table's "dead" radio button filter
Classic component test and harness component test which test the GOT search field filterEnd to end tests
Thus far, our discussion has centered on component tests. However, as noted earlier, they aren't the only type of Angular tests that engage with our HTML and Material components.
End-to-end tests represent another crucial category that falls into this domain. Can we apply Angular Material component harnesses within an end-to-end test?
Absolutely, we can. In fact, we can employ the exact same API. The sole distinction lies in the loader. In our setup, we would obtain the loader from the ProtractorHarnessEnvironment rather than the TestBedHarnessEnvironment.
let loader: HarnessLoader;
beforeEach(() => {
loader = ProtractorHarnessEnvironment.loader();
});
Conclusion
Angular Material exports harness classes that assist us in refining our component tests.
Through the use of harness classes, our component tests become more dependable. We communicate with components via an officially supported API and avoid relying on CSS selectors to access internal DOM structures. Our tests continue to function even if Material decides to refactor its internals.
Moreover, we no longer need to concern ourselves with stabilizing the fixture. Material's component harnesses handle that responsibility automatically.
An additional substantial advantage, which should not be overlooked, is readability. Component tests employing Material's test harness are clearer and more straightforward to grasp.
Do you enjoy the theme of the code preview? Explore our brand new theme plugin
Skol — the ultimate IDE theme
A touch of the northern lights for your editor. This dark theme is straightforward yet impactful, offering a visually pleasing experience that reduces eye strain.
Elevate your UI development with Angular and artificial intelligence
Angular + AI Video Course

A practical video course demonstrating the integration of AI capabilities into Angular applications with Hash Brown, enabling the creation of smart, reactive interfaces.
Topics include streaming chat, tool invocation, generative UI patterns, structured output, and more, all presented in a progressive manner.
Seeking a detailed resource on Angular Signal Forms architecture, validation, and migration strategies?
Angular Signal Forms eBook
Develop robust, type-safe, and production-ready forms using Angular signals and a model-driven approach.
This guide covers schema-based validation, form-state signals, bespoke controls, transitioning from Reactive Forms, and effective API mapping techniques.
Interested in the content and aiming to master Angular's new Signal Forms?
Angular Signal Forms: Hands-On Masterclass

Become proficient in Angular's latest Signal-Forms over the course of 12 progressive chapters that blend conceptual learning with practical exercises.
Gain knowledge on form fundamentals, validators, custom controls, subforms, migration plans, and further topics.
Be the first to know
when new posts go live
Subscribe to the Angular Experts Content Updates & News and you'll get a notification every time a fresh blog post is published on Angular, NgRx, RxJS, or other fascinating frontend topics!
Your email remains private with us, and you have the freedom to unsubscribe whenever you choose!
Responses & comments
Feel free to ask questions, share your insights, and engage with the subject material
You might also like
Dive into these Angular Experts blog posts to broaden your understanding of subjects tied to Angular !

Top 10 Angular Architecture Mistakes You Really Want To Avoid
Angular's evolution continues apace in 2024, yet the core principles hold steady, making solid architecture knowledge a timeless and worthwhile investment!

Tomas Trajan
@tomastrajan
Sep 10, 2024
15 min read

Angular Signal Inputs
Transform your Angular building blocks with the new Reactive Signal Inputs feature.

Kevin Kreuzer
@nivekcode
Jan 24, 2024
6 min read

Improving DX with new Angular @Input Value Transform
Look beyond traditional getters and setters and explore the power of custom transformers or the built-in booleanAttribute and numberAttribute functions.

Kevin Kreuzer
@nivekcode
Nov 18, 2023
3 min read
Unlock the full potential of your projects with our deep expertise
Angular Experts has collaborated with both large enterprises and emerging startups over the years, delivering workshops and tutorials while nurturing a rich ecosystem of open source projects. We are proud of our track record in modern front-end engineering and would be excited to support your growth.
