In my previous blog post, we learned that there were two ways to unit test Angular pipes:

  • isolated tests (without TestBed),
  • integrated tests (with TestBed).

But we put the focus on isolated tests. In this article, we are going to switch our focus on how to write integrated unit tests for Angular pipes.

We talk about integrated unit testing when we are testing the pipe in the same condition that it will be used at runtime. This means that we will test it when used inside an Angular component's template.

The pipe under test

In case, you didn't read my previous blog post, here is the code of the pipe we are going to test. It is a simple pipe that transforms an array to its mean.

The Mean Pipe

  1. Implement the PipeTransform interface.
  2. Return the mean of the array of numbers.

The full source code of this pipe can be found at the end of the article.

We need a host component

Because we want to write integrated tests for our pipe, we need a component that hosts our pipe. A host component is like any other Angular component. There is nothing specific about it. It is just a way to use the pipe in an Angular component's template.

The code for the host component is as follows:

The Host Component for the Test

  1. Define property that holds the values to be passed to the pipe
  2. Display the result of the transformation of those values through the pipe.

Setting up the integrated tests!

Tests Setup

  1. TestBed.configureTestingModule allows us to create a specific Angular module for testing purposes. It accepts pretty much the same parameters as the @NgModule decorator.
  2. Notice that we wrap the body of our first beforeEach inside a special Angular zone. We do so by calling the async function, one of the many Angular testing utilities. We need this because TestBet.compileComponents is asynchronous. So this ensures that our component's template is compiled beforehand (although technically this is not necessary in this example because our template is inlined).
  3. A ComponentFixture is a wrapper around our host component. It allows us to interact with the environment of our component like its change detection or its injector.
  4. The fixture is returned by TestBed.createComponent.
  5. From the fixture, we can get the DebugElement. The DebugElement is a wrapper around our component's HTML element. It provides more functionality than the native HTMLElement.
  6. We can also get the real instance of our component from the fixture.

We made sure that our fixture can be properly instantiated. Now we can start writing real expectations.

The actual tests

The Actual Tests

  1. We start by updating the values property of our component and we call fixture.detectChanges. fixture.detectChanges kicks off change detection for our component. It is necessary if we want our template to reflect the changes we made to the component class.
  2. Next, we use debugElement.query passing it a predicate By.css('div'). This allows us to target the div element by using its CSS selector.
  3. From there, we can get the native HTMLDivElement.
  4. Then we can write our expectations.

The full source code of this pipe can be found at the end of the article.

Conclusion

In this article, we learned how to write integrated unit tests for our Angular pipes. The setup for this kind of is more complicated than for isolated unit tests. There are also more concepts involved because we need to use Angular testing utilities. But the effort is worth it as integrated unit tests can help us detect bugs that isolated tests cannot reveal.


Listing

mean.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'mean'
})
export class MeanPipe implements PipeTransform {

  transform(value: number[]): number {
    if (!Array.isArray(value)) {
      return value;
    }
    if (value.length === 0) {
      return undefined;
    }
    const sum = value.reduce((n: number, m: number) => n + m, 0);
    return  sum / value.length;
  }
}

mean.pipe.integrated.spec.ts

import { MeanPipe } from './mean.pipe';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';


@Component({
  template: '<div>{{ values | mean }}</div>'
})
export class MeanPipeHostComponent {
  values: number[];
}

describe('MeanPipe inside a Component', () => {
  beforeEach(async(() => {
    TestBed
      .configureTestingModule({
        declarations: [MeanPipe, MeanPipeHostComponent]
      })
      .compileComponents();
  }));

  let fixture: ComponentFixture<MeanPipeHostComponent>;
  let debugElement: DebugElement;

  let component: MeanPipeHostComponent;

  beforeEach(() => {
    fixture = TestBed.createComponent(MeanPipeHostComponent);
    debugElement = fixture.debugElement;
    component = fixture.componentInstance;
  });

  it('should create an instance', () => {
    expect(fixture).toBeTruthy();
  });

  it('should display 1', () => {
    component.values = [1, 1];
    fixture.detectChanges();

    const div: HTMLDivElement = debugElement
      .query(By.css('div'))
      .nativeElement;

    expect(div.textContent.trim()).toEqual('1');
  });

  it('should display 0', () => {
    component.values = [1, -1];
    fixture.detectChanges();

    const div: HTMLDivElement = debugElement
      .query(By.css('div'))
      .nativeElement;

    expect(div.textContent.trim()).toEqual('0');
  });

  it('should display nothing', () => {
    component.values = [];
    fixture.detectChanges();

    const div: HTMLDivElement = debugElement
      .query(By.css('div'))
      .nativeElement;

    expect(div.textContent.trim()).toEqual('');
  });
});


If you enjoyed this article, follow @ahasall on Twitter for more content like this.