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

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Nov 15, 2022

6 min read

Angular Material component harnesses
share

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.
Angular Material component harnesses - Angular Experts — figure 3
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.

Angular Material component harnesses - Angular Experts — figure 4 Game of Thrones characters filter table

This 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.
Angular Material component harnesses - Angular Experts — figure 5
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/await statements instead of callbacks. Then we don't have to call the done function.

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 HarnessEnvironment is the foundation of our harness test. There are different types of HarnessEnvironment's. For Karma/Jasmine environment we use the TestbedHarnessEnvironment.

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?

Angular Material component harnesses - Angular Experts — figure 6 Classic component test and harness component test which test the GOT character table's "dead" radio button filterAngular Material component harnesses - Angular Experts — figure 7 Classic component test and harness component test which test the GOT search field filter

End 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

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

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

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

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.

Win win deal illustration

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!

Emails might include extra promotional material; consult our Privacy policy for specifics.

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 !

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.