Tidy up your tests using component test harnesses (1/3)
Tags: angular, webdev, tutorial, testing
Published: 2021-11-29 15:50:00

---
title: Tidy up your tests using component test harnesses (1/3)
published: true
description: What are test harnesses and how they help simplify your component automated tests.
tags: angular,webdev,tutorial,testing
series: tidy-your-unit-tests-with-test-harnesses 
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ha02qavmwsbcoawjam1q.jpg
---

One of the things I love most about Angular is that testing is a first-class citizen of the framework. But, interacting with UI components in automating testing can still be tedious. You may spend more time worrying about *HOW* to write a test instead of focusing on testing the interaction. Your tests may still be difficult to read and understand at a glance, and your tests may depend on the UI component libraries' internal selectors, which may change. :grimacing:

:sparkles: You can tidy up your tests and focus on writing meaningful tests using component test harnesses. :sparkles:

![The benefits of test harnesses](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5jbo6ztrc1de0p2n069.jpg)

## Test harnesses

Test harnesses are part of the testing APIs in `@angular/cdk/testing` library, in the [Angular **C**omponent **D**evelopment **K**it](https://material.angular.io/cdk/categories)(**CDK**). The CDK testing library supports testing interactions with components. The idea for test harnesses comes from the [`PageObject` pattern](https://martinfowler.com/bliki/PageObject.html), used for integration style testing.

> A page object wraps an HTML page, or fragment, with an application-specific API, allowing you to manipulate page elements without digging around in the HTML.
<cite>– Martin Fowler, PageObject</cite>

### Component test harnesses

UI components then implement the CDK's test harness APIs to create a component test harness. When there is a component test harness, it allows a test to interact with the component in a supported way.

Component test harnesses can

1. Make your tests easier to read and understand 
2. Make your tests easier to write by using the APIs to interact with UI components
3. Make your tests more resilient because you don't depend on the  internals of a UI component

You'll have tidy tests that are less brittle. :heart_eyes:

![Component test harnesses in practice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w4503iprgt7fe9cmgutz.jpg)

## Testing with component test harnesses

The CDK test harness loader supports two environments — unit and e2e. Out of the box, you have support for loading test harnesses in unit tests using Karma and e2e tests using Protractor. If your favorite testing library is something different, the API allows creating test harness environments.

Angular Material is a UI component library maintained by the Angular team. All Angular Material components provide test harnesses in Angular Material components version 12. However, the effort started in version 9, so if you aren't on the latest version of Angular, you might have access to some component test harnesses.

### A side by side comparison of tests

Let's look at an example unit test and compare a test with and without test harnesses. We'll look at [a sample To-do app written using Angular Material UI components](https://alisaduncan.github.io/component-harness-code/).

![Tidy-task todo app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mr6qcd0nm7sewcclxsar.jpg)

We'll focus on testing the behavior of applying a CSS class that draws a strikethrough on the checkbox text of completed tasks.

_This post assumes knowledge of [building a site using Angular](https://angular.io/start) and [writing unit tests using Karma](https://angular.io/guide/testing). The examples shown are a simplified version from the project GitHub repo._

{% github alisaduncan/component-harness-code no-readme %}

### The code we'll test

We're focusing on the checkbox element and adding a `ngClass` attribute to conditionally add the CSS class `.task-completed` when the task is complete. The `.task-completed` CSS class adds a strikethrough on the text.

![To-do list tasks showing a strikethrough when task is checked](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mn57j4opaolvpha6kk06.jpg)

If you haven't used Angular Material before, all components have a `mat` prefix, so a checkbox becomes `mat-checkbox`. A snippet of code to display a to-do task and handle the strikethrough behavior for a `MatCheckbox` component looks something like this.

```html
<mat-checkbox
  #task
  [ngClass]="task.checked ? 'task-completed' : ''">
      {{todo.description}}
</mat-checkbox>
```

### What we'll test

We'll do the following operations in the test:

1. Access the checkbox element
2. Assert the checkbox starts unchecked
3. Assert the checkbox doesn't contain the CSS class `task-completed`
4. Toggle the checkbox to mark as checked
5. Assert the checkbox is now checked
6. Assert the checkbox now contains the CSS class `task-completed`

### A test without harnesses

Let's start with what an example test for this logic might look like without test harnesses. We'll skip the `TestBed` setup and dive right into the test.

```ts
it('should apply completed class to match task completion', () => {

   // 1. Access mat-checkbox and the checkbox element within
   const matCb = fixture.debugElement.query(By.css('mat-checkbox'));
   expect(matCb).toBeTruthy();

   const cbEl = matCb.query(By.css('input'));
   expect(cbEl).toBeTruthy();

   // 2. Assert the checkbox element is not checked  
   expect(cbEl.nativeElement.checked).toBe(false);

   // 3. Assert the mat-checkox doesn't contain the CSS class
   expect(matCb.nativeElement.classList).not.toContain('task-completed');
 
   // 4. Toggle the mat-checkbox to mark as checked
   const cbClickEl =
      fixture.debugElement.query(By.css('.mat-checkbox-inner-container'));
   cbClickEl.nativeElement.click();
   fixture.detectChanges();
 
   // 5. Assert the checkbox element is checked
   expect(cbEl.nativeElement.checked).toBe(true);

   // 6. Assert the mat-checkbox contains the CSS class
   expect(matCb.nativeElement.classList).toContain('task-completed');
});
```

There's a lot of selectors and querying the DOM going on here. To access the checkbox element and interact with it, we get

* the checkbox element itself (`mat-checkbox`), which has the bindings for the attribute directive
* the input element (`input` within the `mat-checkbox` element), which is the checkmark
* the CSS selector `.mat-checkbox-inner-container`, which is the clickable element of the `mat-checkbox`

With these three elements, we can proceed with testing operations. But to identify how to write this test, we had to look at the inner workings of `mat-checkbox` implementation and use potentially non-supported selectors, which could change in the future.

### A test with component test harnesses

Let's contrast this with a test using `MatCheckbox` component test harnesses. To make it easier to compare, we'll follow the same order of operations.

Here's the same test using `MatCheckbox` test harnesses

```ts
it('should apply completed class to match task completion', async () => {

   // 1. Access the mat-checkbox
   const cbHarness = await loader.getHarness(MatCheckboxHarness);

   // 2. Assert the checkbox element is not checked. 
   expect(await cbHarness.isChecked()).toBeFalse();
 
   // 3. Assert the mat-checkox doesn't contain the CSS class
   const cbHost = await cbHarness.host();
   expect(await cbHost.hasClass('task-completed')).not.toBeTrue();
 
   // 4. Toggle the mat-checkbox to mark as checked
   await cbHarness.toggle();
 
   // 5. Assert the checkbox element is checked
   expect(await cbHarness.isChecked()).toBeTrue();

   // 6. Assert the mat-checkbox contains the CSS class
   expect(await cbHost.hasClass('task-completed')).toBeTrue();
});  
```

Notice this test is a lot shorter, **a lot** easier to read, and we didn't have to worry about digging into the inner workings of the `MatCheckbox` code to write this test. Everything we did was via the public API of the `MatCheckboxHarness`.

![Test harnesses make tidier tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52mzth3h2rv9vim5bf0r.jpg)

## The value of test harnesses

Now that we compared an example test with and without harnesses, we see the value the test harnesses provide. With component test harnesses, we're able to focus on testing behaviors and better communicate the goal of the test.

In tomorrow's post, we'll dive into the `@angular/cdk/testing` API to better understand what we get from the library.

Let me know in the comments below if you write component tests and what techniques you use, such as `PageObjects` or something else.