Tidy up your tests using component test harnesses (2/3)

In the last post, we learned about test harnesses and the value of using Component test harnesses in our automated tests. Let's take a closer look at the @angular/cdk/testing API to understand how we work with component test harnesses.

Check out the first in this series ⬇️

Loading test harnesses for the environment

Test harnesses are available for e2e and unit test environments. Out of the box, the @angular/cdk/testing supports test harnesses in Protractor for e2e and Karma for unit tests. If you use other testing frameworks, you'll have to find an implementation of TestBedHarnessEnvironment for your framework of choice.

We'll use the Karma unit test environment for our code examples.

This post assumes knowledge of building a site using Angular and writing unit tests using Karma. The examples shown are a simplified version from the project GitHub repo.

GitHub logo alisaduncan / component-harness-code

Sample app with unit tests with and without test harnesses, and a custom component test harness for the component test harness presentation

The HarnessLoader

To initialize the environment and create a HarnessLoader for your test suite, you'll set it up as part of your TestBed setup by passing in the ComponentFixture instance.

import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';

describe('Tidy Test', () => {
  let fixture: ComponentFixture<TidyTestComponent>;
  let loader: HarnessLoader;

  beforeEach(() => {
    TestBed.configureTestingModule({...});
    fixture = TestBed.createComponent(TidyTestComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
  });
});

The above code creates a HarnessLoader compatible with elements contained inside the fixture's DOM root. If you need to test an element outside the fixture's DOM root, such as a dialog, you can create a HarnessLoader at the DOM root.

const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);

Get component test harnesses

Now that we have the HarnessLoader, we can get the component test harnesses we want to interact with. The HarnessLoader and component test harnesses return promises, so you'll use the async/await pattern.

You can get an individual test harness or all test harnesses.

Individual test harness
Get an individual component test harness by specifying the element type you're looking for using the getHarness method. The method returns a test harness for the first requested test harness element type it comes across or rejects the promise if none exists.

const btn: MatButtonHarness = await loader.getHarness(MatButtonHarness);

All test harnesses
Get all component test harnesses of a type by using the getAllHarnesses method. In this example, the method returns an array of the requested test harness element type MatButtonHarness.

const btns: MatButtonHarness[] = await loader.getAllHarnesses(MatButtonHarness);

You can also create child loaders for certain sections of your template so you can focus on getting test harnesses. Create a child loader by passing in a selector and then get either an individual test harness or all test harnesses

const childLoader: HarnessLoader = await loader.getChildLoader('.my-selector');
const

Applying filters

To specify which component test harness you want to get, you can add a filter. The filtering capability and what you can filter on are unique for each component test harness. The MatButtonHarness can filter by text to find all MatButtonHarness with the text "delete" like this.

const btns: MatButtonHarness[] = await loader.getAllHarnesses(MatButtonHarness.with({text: 'delete'}));

Most Material component harnesses have filters built-in. So you can get all checked or unchecked MatCheckBoxHarness, for example. You will have to read each component test harness documentation or inspect its API.

The host element

All test harnesses have a TestElement host. The TestElement is like the DebugElement's nativeElement. With the TestElement, you can hover, click, blur, access class list and dimensions, and more! Access the TestElement like this

const btn: MatButtonHarness = await loader.getHarness(MatButtonHarness);
const btnHost: TestElement = await btn.host();
await btn.hover();

Interacting with the component

We covered the essential functions to get test harnesses and interact with the underlying host element. How about working with the component test harness itself, such as checking the checkbox or selecting a menu item? Functions like these are unique to each component, so you'll have to refer to the API for the component test harness. Angular Material components have documentation for the component test harness so that you can look at the API overview for the component you're interested in on their site.

Optimizing test harness interactions

We've been using async/await for all the test harness interactions. We can optimize the interactions using a helper method, parallel, which parallelizes the async operations and optimizes for change detection! For example, you can get the checked status and the label for a checkbox in one go like this.

const checkbox: MatCheckboxHarness = await loader.getHarness(MatCheckboxHarness);
const [checked, label] = await parallel(() => [
  checkbox.isChecked(),
  checkbox.getLabelText()
]);

Expanding beyond Angular Material Components

We learned about using the HarnessLoader to get test harnesses and ways we can interact with them. But what if you don't work with Material? Or what if you have custom UI components in your app? You can use the @angular/cdk/testing API to create custom component harnesses. We'll talk about how to do that in the next post in the series.

Have you used Angular Material component test harnesses? Let me know in the comments below if you have and what your thoughts are.

29