What problem Angular Material component harnesses solve and how they help you create dependable, stable, and clean test suites for your UI components.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Feb 14, 2023

7 min read

Angular Material components testing
share

A well-crafted test suite is the foundation of any solid application. It guarantees that the app behaves as intended, and a dependable set of tests also makes refactoring safer. When tests are trustworthy, we can modify the internals of our code without worrying about breaking existing functionality.
Here, we’ll guide you through the most effective approach for testing Angular Material components.

Testing in Angular

In Angular, we distinguish between different types of tests; unit tests, component tests (integration tests) and end to end tests.
Angular Material components testing - Angular Experts — figure 3
The testing pyramid for Angular applications

Unit tests are the simplest kind to write—they target only the functions in our TypeScript code. Component tests and end-to-end tests, however, present more of a challenge. They exercise not just the TypeScript, but also the template, which can depend on third-party packages like Material.

So what exactly makes component tests hard today? 🤔

For components that use Angular Material, our tests typically need a CSS query selector to reach the relevant elements.

To see what this means in practice, let’s inspect a few of the tests from a “Game of Thrones” filter demo app.

Angular Material components testing - Angular Experts — figure 4 Game of Thrones characters filter table

This app is quite simple: using the radio buttons, you can narrow down the characters based on whether they are alive or not. Moreover, typing something into the input field applies an extra layer of filtering to the list.

Take the “Dead” radio button, for instance—clicking it instantly ends up showing only the entries that match that status.

Angular Material components testing - Angular Experts — figure 5 filtered (dead) game of thrones characters

Now we’ll put together a unit test for the radio button filter. The test verifies that the expected method gets invoked and that the data source is filtered in the proper way. It exercises the connection between our template, Material, and the 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();
  });
});

Try out filtering the GOT character table through the radio button to test the filter logic.

Initially, we grab the radio button identified by deadFilter. Calling a click on that radio button alone yields no effect. So we locate the actual clickable part within it, which is the element carrying the mat-radio-container class.

Once we trigger the click method on that element, we invoke fixture.detectChanges() so the TestBed refreshes the bindings. Then we await the promise originating from fixture.whenStable(). Waiting for that promise ensures JavaScript’s task queue has drained completely.

Now we can fetch the table rows and check their count. Because we’re inside a then handler, we must use Jasmine’s done callback to confirm the test ends only after our checks run.

This method has some drawbacks, though—maybe you’ve spotted them already. 😉

It depends on internal implementation

Selecting a radio button doesn’t just require accessing it and performing .click on its nativeElement. Instead, we must search for the inner clickable element within the radio component.

const deadRadio = fixture.debugElement.query(By.css('#deadFilter'));
const clickableElement = deadRadio.query(By.css('.mat-radio-container'));

clickableElement.nativeElement.click();

A test snippet that clicks a Material radio button

Using CSS selectors to target Material components is problematic for several reasons;

To begin with, you have to inspect the components themselves. Locating the interactive element requires digging through their inner structure.

Moreover, if Material changes how the radio button is rendered internally, such as renaming the mat-radio-container class to mat-radio-box, your test breaks. This happens even though your app still works as intended.

Depending on third-party library implementation details is troublesome since you must understand those details and remain susceptible to their internal changes

Manually stabilize the fixture

Once Angular has resolved all bindings and the JavaScript task queue is empty, a fixture is considered stable. Ensuring this stability requires explicitly calling fixture.detectChanges followed by fixture.whenStable.

fixture.detectChanges();
fixture.whenStable().then(() => {
  // ...
});

Invoke fixture.detectChanges followed by fixture.whenStable to ensure the view is fully rendered.

Remembering to add those calls can be tedious. “Skipping them often leads to lengthy, confusing debugging sessions” (burned child 😉).

Signal completion with done

Whenever assertions are made inside an asynchronous callback—like those from a Promise or Subscription—we must invoke the done function after the checks. Failing to do so can skip the assertions entirely, which could cause a test to pass even when the actual behavior is wrong.

Tipp: With promises, we can opt for async/await syntax instead of callbacks, eliminating the need to call done altogether.

Angular Material test harness to the rescue ⛑️

The harness concept draws from the PageObject pattern, offering a class that allows a test to engage with a component via a formal API.

Leveraging component harnesses shields a test from the component library’s internal workings, making it resilient to internal changes.

Interested in using Material’s test harness? Here’s how.

let loader: HarnessLoader;

beforeEach(() => {
  fixture = TestBed.createComponent(FilterTableComponent);
  component = fixture.componentInstance;
  loader = TestbedHarnessEnvironment.loader(fixture);
  fixture.detectChanges();
});

Create a HarnessLoader

To start, we need a loader. So, in the outermost describe block, we declare a variable and then initialize it within the beforeEach hook. The TestbedHarnessEnvironment provides us with a HarnessLoader instance.

Every harness test rests on a HarnessEnvironment. Multiple HarnessEnvironment types exist; for a Karma/Jasmine setup, the TestbedHarnessEnvironment is the one to use.

That completes the setup. With this in place, we can now leverage component harnesses in our test. Next, let’s rework the filter test from before.

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);
});

A component test that verifies the filter radio button behavior using Angular Material component harnesses

Notice how straightforward and clear our test now appears! 🤩

Harness-based testing often involves async/await. As a rule of thumb, the initial step is to always prefix our callback with the async keyword.

By using the loader, we obtain access to harness objects. For this scenario, we specifically target the radio button harness marked “Dead”. Once the harness is in hand, its API provides the check method. That method is intuitive and already handles the underlying checkbox operations — eliminating any need to dig into mat-radio-button internals.

The same approach applies to the table. Since our page contains just one table, fetching the MatTableHarness without a selector is adequate. After acquiring the MatTableharness, invoking the getRows method yields the row count.

Also worth highlighting: we have not used fixture.detectChanges() or fixture.whenStable(). The component harness automatically stabilizes the fixture with each interaction. This is a major win, reducing the likelihood of test flakiness.

Readability is another key advantage. Compare the two snippets below — your thoughts? Which test reads more easily, the standard test or the harness test? Which one has minimal boilerplate code?

Angular Material components testing - Angular Experts — figure 6 Testing the “dead” radio button filter on the GOT character table using a classic component test and a harness component test Angular Material components testing - Angular Experts — figure 7 Classic component test and harness component test which test the GOT search field filter

With harness testing in Angular, component testing becomes simpler, more readable, and more effective.

If you like what you see in the code preview, check out our brand new theme plugin

Skol - the ultimate IDE theme

Skol - the ultimate IDE theme

Bring the aurora borealis experience directly into your development environment. A straightforward yet robust dark theme that enhances visual appeal while easing eye strain.

Create more intelligent user interfaces by combining Angular with artificial intelligence

Video Training on Angular and AI Integration

Angular + AI Video Course

This is a practical training program that demonstrates the incorporation of artificial intelligence into Angular applications, leveraging Hash Brown to craft responsive, intelligent interfaces.

Work through live chat sessions, function invocation, AI-driven interface generation, structured output handling, and further topics — all in a progressive manner.

Create more intelligent user experiences with Angular and AI

Interactive Angular + AI Learning Program

Angular + AI Video Course

This practical course demonstrates how to bring artificial intelligence into Angular apps with Hash Brown, creating responsive and intelligent interfaces.

Explore streaming conversations, invoking tools, generating UI components, structured outputs, and much more — all with guided, incremental lessons.

Enjoying the material and want to dive deeper into Angular's new Signa Forms?

Angular Signal Forms: A Step-by-Step Deep Dive

Angular Signal Forms: Hands-On Masterclass

Work through Angular's latest Signal-Forms feature in 12 step-by-step chapters that combine theoretical insights with practical exercises.

Dive into the essentials of form creation, validation logic, bespoke controls, nested forms, and approaches for transitioning from older patterns.

Win win deal illustration

Stay in the loop
with fresh articles

Join the Angular Experts Content Updates & News mailing list, and every time we publish something new on Angular, Ngrx, RxJs, or other fascinating Frontend topics, you'll be the first to know.

Your email stays private, and unsubscribing is a breeze whenever you want!

Be aware that emails might occasionally carry additional promotional material—check our Privacy policy for all the details.

Join the conversation

Feel free to ask anything or share your thoughts and insights about the topic at hand

You might also like

Dive into more blog posts from the Angular Experts team to discover deeper insights on subjects such as Angular !

Top 10 Angular Architecture Mistakes You Really Want To Avoid

Top 10 Angular Architecture Mistakes You Really Want To Avoid

In 2024, Angular keeps changing for better with ever increasing pace, but the big picture remains the same which makes architecture know-how timeless and well worth your time!

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

Sep 10, 2024

15 min read

Angular Signal Inputs

Angular Signal Inputs

Revolutionize Your Angular Components with the brand new Reactive Signal Inputs.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Jan 24, 2024

6 min read

Improving DX with new Angular @Input Value Transform

Improving DX with new Angular @Input Value Transform

Embrace the Future: Moving Beyond Getters and Setters! Learn how to leverage the power of custom transformers or the build in booleanAttribute and numberAttribute transformers.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Nov 18, 2023

3 min read

Leverage our deep expertise to scale your team

Our consultants at Angular Experts have accumulated years of hands-on experience partnering with both corporate clients and emerging startups, delivering workshops and tutorials, and curating a broad ecosystem of open source projects. We are proud of our track record in cutting-edge front-end development and would be excited to contribute to your company's growth.