Are you working with Angular?  Have you written hundreds of unit tests and do you feel like it is taking way more time to write the tests than it did to write the application code?  Let’s talk about ways to speed up the authoring of those Angular unit tests and get back to doing the fun stuff.

Simplify DOM interactions with Harnesses

Angular components interact with users through the DOM and you want your automated tests to do the same.  Here is an example of some test code that clicks a save button.

See the Angular Components Library: More Than Just Material post for more information about how components work.

// Query the DOM for the save button
const cancelButton = fixture.debugElement.query(By.css('button.my-save-button')); 
// Click the save button
cancelButton.nativeElement.click();  
// Tell the test fixture to detect changes                                               
fixture.detectChanges();

By itself, that code isn’t terrible. Let’s say you have a half dozen unit test cases that need to test scenarios around clicking that save button.

it('scenario 1', () => {
  // set scenario 1 preconditions.
  const cancelButton = fixture.debugElement.query(By.css('button.my-save-button'));
  cancelButton.nativeElement.click();
  fixture.detectChanges();
  // test for side effects
});
it('scenario 2', () => {
  // set scenario 2 preconditions.
  const cancelButton = fixture.debugElement.query(By.css('button.my-save-button'));
  cancelButton.nativeElement.click();
  fixture.detectChanges();
  // test for side effects
});
it('scenario 3', () => {
  // set scenario 3 preconditions.
  const cancelButton = fixture.debugElement.query(By.css('button.my-save-button'));
  cancelButton.nativeElement.click();
  fixture.detectChanges();
  // test for side effects
});
it('scenario 4', () => {
  // set scenario 4 preconditions.
  const cancelButton = fixture.debugElement.query(By.css('button.my-save-button'));
  cancelButton.nativeElement.click();
  fixture.detectChanges();
  // test for side effects
});

If the component’s DOM ever changes, look at how many CSS selectors for the save button you will need to update.  Yes, you could create a clickSaveButton method and use that in each test case but you may end up duplicating that in your end-to-end tests.  There is a better way.

Angular CDK component harnesses allow you to create an API for your automated tests to use when interacting with a component’s DOM.  Component harnesses will protect your unit tests and end-to-end tests from DOM structure changes.  Here is our first test case after creating a harness for our save button:

it('scenario 1', async () => {
  // set scenario 1 preconditions.
  await harness.clickSaveButton();
  // test for side effects
});

Our refactored test case contains no CSS queries and no calls to the test fixture to detect changes.  The harness takes care of all of that.  Harnesses can also be used in end-to-end tests with all the same benefits.  Take note that the calls to a component harness are asynchronous so my test case is now using async/await.

Use a harness to read text and attributes from HTML elements as well as interact with form inputs.  All interactions with the DOM should be done through a harness. 

Simplify creating mock components for unit tests

When an Angular component includes child components, those component dependencies should be mocked in the parent component’s unit test.  That will make sure the unit test is really a unit test.  From a developer ergonomics perspective, it stops a component modification from rippling through hundreds of other seemingly unrelated tests.

Mock components can be a pain to maintain.  Each mock component has to mirror the original component’s inputs and outputs or else the parent component being tested will not be able to interact with the mock component resulting in errors.  

Here is an example component:

@Component({
  selector: 'my-component',
  templateUrl: './my.component.html',
  styleUrls: [ './my.component.css' ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent implements OnInit  {
  @Input()
  name: string;

  @Input()
  item: Item;

  @Output()
  someEvent = new EventEmitter<string>();

  ngOnInit() {
    // Do some init stuff.
  }

  // view model builders, click handlers, etc.
}

Here is a mock component for that example component:

@Component({
  selector: 'my-component',
  template: ''
})
export class MyMockComponent {
  @Input()
  name: string;

  @Input()
  item: Item;

  @Output()
  someEvent = new EventEmitter<string>();
}

And here is an example of using that mock component to initialize a unit test for a component named MyParentComponent:

TestBed.configureTestingModule({
  declarations: [ MyParentComponent, MyMockComponent ]
}).compileComponents();

If I want to modify the inputs or outputs in MyComponent, then I will need to remember to update my mock component as well.

The ng-mocks library (URL: https://github.com/ike18t/ng-mocks) will help us write unit tests quicker and make those tests easier to maintain.  When writing a component unit test, use the MockComponent factory function from the ng-mocks library to mock all of the dependent components.

TestBed.configureTestingModule({
  declarations: [ MyParentComponent, MockComponent(MyComponent) ]
}).compileComponents();

That’s it. I don’t need to maintain a mock component at all.  As the API for MyComponent changes, the mock component declaration in the test configuration above will change automatically.  Also, test cases can query for the original component rather than the mock component.

childDebugElement = fixture.debugElement.query(By.directive(MyComponent));

Eliminate boilerplate code in unit tests

Writing good unit tests for an Angular component can be time-consuming.  There is boilerplate code involved in the setup of a unit test environment.

@Component({
  template: `
    <my-component
      [name]="name"
      [item]="item"
      (someEvent)="someEventData = $event"
    ></my-component>
  `
})
class MyTestHostComponent {}

beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [ MyTestHostComponent, MyComponent ]
  }).compileComponents();

  hostFixture = TestBed.createComponent(MyTestHostComponent);
  hostComponent = fixture.componentInstance;
  component = hostFixture.debugElement.query(By.directive(MyComponent));
  hostFixture.detectChanges();
});

it('should respond to input changes', () => {
  hostComponent.name = 'new name',
  hostFixture.detectChanges();
  // ... look for DOM changes ...
});

The ngneat/spectator library was created to allow you to configure your test environments quickly and with a lot less code.

@Component({ template: '' })
class MyTestHostComponent {
  name: string;
  item: Item;

  someEventData: EventDataType | null = null;
}

const createHost = createHostFactory({
    component: MyComponent,
    host: MyTestHostComponent,
    template: `
    <my-component
      [name]="name"
      [item]="item"
      (someEvent)="someEventData = $event"
    ></my-component>
  `
});

it('should create', async () => {
  const spectator = createHost();
  expect(spectator.component).toBeTruthy();
});

it('should respond to input changes', () => {
  spectator.setHostInput('name', 'new name');
  // ... look for DOM changes ...
});

Not only is TestBed initialization simplified, spectator takes care of all of the change detection calls as well when updating component inputs.  Notice there are no calls to detectChanges().  A side effect of removing all of the boilerplate code is code reviews are easier.  There’s less fatigue sifting through setup code in a review.

In addition, Angular CDK component harnesses can be used with spectator to interact with the DOM.  Notice in the following example, the host component is used to detect that events occurred.

async function getHarness(
  spectator: SpectatorHost<MyComponent, MyTestHostComponent>) {
    const harnessLoader = TestbedHarnessEnvironment.loader(spectator.hostFixture);
    return await harnessLoader.getHarness(MyComponentHarness);
}

it('should emit an event when something is clicked', async () => {
  // Create the spectator and pass data to the host component.
  const spectator = createHost(undefined, {
      hostProps: {
        name: 'First Last',
        item: { ... some item object ... }
      }
    });

  const myComponentHarness = await getHarness(spectator);

  // Interact with the harness…
  await myComponentHarness.clickSomething();

  // Look for some event data.
  expect(spectator.hostComponent.someEventData).toEqual({ ... some data ... ));
});

Summary

Using harnesses, ng-mocks, and spectator can reduce the time it takes to write your unit tests allowing you to focus on creating high-quality tests. If you’re at square one and you’d like to gain these efficiencies sooner rather than later, contact us. Our experienced engineers will help you devise a plan and approach that will get you and your team on your way to better testing practices today!