Angular
Component Test Harnesses FTW!

A familiar tale

A familiar tale

Alisa Duncan

  • Senior Developer Advocate Okta
  • Angular GDE
  • K-Drama fan

What are we talking about?

🤔

Tests UI interactivity within an Angular application using an unit testing library, but concepts apply to all forms of testing

A test harness encapsulates implementation details of HTML fragments into an API for the purposes of testing

How test harnesses help you

  • Ignore inner workings of the encapsulated component
  • Easier to read and write tests
  • Less test maintenance

Consumers of Angular Material components have a head start!

Consumers of Angular Material components have a head start!

All Angular Material components have test harnesses starting in v12

The new MDC changes in v15 means your old tests might need updates

Let's see test harnesses in action

The component template

Interact with input element


            it('no test harness', async () => {
            });
          

Interact with input element


            it('no test harness', async () => {
              const input = fixture.debugElement.query(By.css('input'));
              expect(input).not.toBeNull();
              input.nativeElement.value = 'email@email.email';
              input.nativeElement.dispatchEvent(new Event('input'));
              fixture.detectChanges();
              await fixture.whenStable();
            });
          

Working with the input element harness


            it('yay test harness!', async () => {
            });
        

Working with the input element harness


            it('yay test harness!', async () => {
              const emailInputEl = await loader.getHarness(MatInputHarness);
              await emailInputEl.setValue('email@email.email');
            });
        

Identifying selectors can be painful

Identifying selectors can be painful


          it('interact with slide toggle', () => {
            const slideToggleEl
              = fixture.debugElement.query(By.css('mat-slide-toggle'));
          });
        

Identifying selectors can be painful


          it('interact with slide toggle', () => {
            const slideToggleEl
              = fixture.debugElement.query(By.css('mat-slide-toggle'));

            const slideToggleBtn
              = slideToggleEl.nativeElement.querySelector('button');
          });
        

Identifying selectors can be painful


          it('interact with slide toggle', () => {
            const slideToggleEl
              = fixture.debugElement.query(By.css('mat-slide-toggle'));

            const slideToggleBtn
              = slideToggleEl.nativeElement.querySelector('button');

            const slideToggleCheck
              = slideToggleEl.nativeElement.querySelector('input[type="checkbox"]');
          });
        

Identifying selectors is a piece of 🍰


          it('interact with slide toggle test harness', async () => {
            const slideToggleHarness
              = await loader.getHarness(MatSlideToggleHarness);
            await slideToggleHarness.toggle();
          });
        

When using test harnesses

  • Tests are easier to read and write
  • Tests have more stability and resiliency
  • Allows you to focus on testing the behaviors that matter

The CDK testing API

The CDK testing API

The Angular CDK testing library supports testing interactions with components

Setting up the TestBed


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

          describe('KDrama Component Test Harness', () => {
            let loader: HarnessLoader;

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

The TestbedHarnessEnvironment

Supports unit testing using Karma

The HarnessLoader


        

Source from cdk/testing/component-harness.ts

Getting harnesses in practice


            const input: MatInputHarness
              = await loader.getHarness(MatInputHarness);

            const input: MatInputHarness|null
              = await loader.getHarnessOrNull(MatInputHarness);

            const inputs: MatInputHarness[]
              = await loader.getAllHarnesses(MatInputHarness);

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

Filtering for a harness


          loader.getHarness(MatInputHarness.with({options}));
        

Your options depend on the harness you are using

The InputHarnessFilters interface

Source from material/input/testing/input-harness-filters.ts

Filter MatInputHarness by value


          const inputs: MatInputHarness[] = await loader.getAllHarnesses(
            MatInputHarness.with({
              value: 'only cool comments, please!'
            })
          );
        

Once you have a harness, it is up to the API of that harness on interactions you can take

Check out the documentation!

An example component test harness

Not quite accurate since MatInput extends from MatFormFieldControlHarness, but simplifying for this presentation

The ComponentHarness base class

Source from cdk/testing/component-harness.ts

The TestElement

Source from cdk/testing/test-element.ts

Notice everything is async

Why async?

  • You always have the latest state on the control
  • It handles change detection for us
  • To help, the CDK library has a handy helper method parallel that we can use
    
                const input = await loader.getHarness(MatInputHarness);
                const [name, value] = await parallel(() => [
                  input.getName(),
                  input.getValue()
                ]);
              

You're now winning at writing tests with Material component test harnesses! Let's win some more!

Write custom component test harnesses for
Maximum Winning!

Custom test harness minimal example

Custom test harness minimal example

Shared component code snippet

Define the filters interface to support querying


            import { BaseHarnessFilters } from '@angular/cdk/testing';

            export interface AddCommentHarnessFilters extends BaseHarnessFilters {
            }
          

Define the filters interface to support querying


            import { BaseHarnessFilters } from '@angular/cdk/testing';

            export interface AddCommentHarnessFilters extends BaseHarnessFilters {
              comment?: string;
            }
          

Implement your component test harness


            import { ComponentHarness } from '@angular/cdk/testing';

            export class AddCommentHarness extends ComponentHarness {
            }
          

Implement your component test harness


            import { ComponentHarness } from '@angular/cdk/testing';

            export class AddCommentHarness extends ComponentHarness {
              static hostSelector = 'app-add-comment';
            }
          

Implement your component test harness

Implement your component test harness

Implement your component test harness

Implement your component test harness

Implement your component test harness


          

Test your component test harness

  • Treat it like publishing an API
  • Test your harness using a test host

ComponentHarness locator methods scale with your component complexity

When writing components, small is winning

Recipe for winning

  • Test harnesses keep UI testing cleaner and easier to maintain
  • Use harnesses when testing with Material components
  • Write test harnesses for shared components
  • Test your test harness - treat it like an API!

Questions?

@AlisaDuncan

@AlisaDuncan #testHarnessesFTW