Egor Panok

Full Stack JavaScript Developer

Unit Testing of Angular Pipes

January 30, 2019 - 7 min read

Pipes

A quick demo how to implement isolated and shallow tests for Angular pipes.

Use Case

Let’s imagine, we have to implement an Angular pipe which shows the duration from some moment in the past until the current moment in human-readable manner. For instance, if an event happened a few seconds ago, our pipe will return ‘a few seconds ago’ instead of the exact date. It’s very natural for humans to operate such durations in that manner and many good apps employ that approach, i.e. Trello:

Trello

Human-readable way to show the durations

Let’s create the pipe and implement simple and shallow tests for it.

The Pipe

Fortunately, the exact function to translate durations into human-readable format already exists. Guess who implemented it? Right, the guys who created momentJS. Not sure if they were the only devs on Earth who did that, but anyway. The function we need is humanize().

So, after installing the moment’s package, the pipe’s code looks this way:

// humanized-compared-with-today-duration.pipe.ts

import { Pipe, PipeTransform } from "@angular/core"
import * as moment from "moment"

@Pipe({
    name: "humanizedComparedWithTodayDuration"
})
export class HumanizedComparedWithTodayDurationPipe implements PipeTransform {
    transform(value: any, args?: any): any {
        return moment.duration(moment(value).diff(moment())).humanize(true)
    }
}

Isolated Pipe Tests

Isolated pipe tests require importing just the pipe and momentJS. We don’t need the TestBed and its companions.

// humanized-compared-with-today-duration.pipe.spec.ts

import { HumanizedComparedWithTodayDurationPipe } from "./humanized-compared-with-today-duration.pipe"
import * as moment from "moment"

Let’s create a new instance of the pipe for each test case using beforeEach() section. The ‘create an instance’ test just checks if the pipe was created - this test is included in the test file boilerplate if you generated the pipe via Angular CLI.

The ‘should humanize the duration between now and few seconds ago’ test generates a date 15 seconds ago from the current moment and then we use this date as an input of the pipe and compare the result with the expected value.

// humanized-compared-with-today-duration.pipe.spec.ts

// ...
describe("HumanizedComparedWithTodayDurationPipe", () => {
    describe("Isolated HumanizedComparedWithTodayDurationPipe tests", () => {
        let pipe, pastDate

        beforeEach(() => {
            pipe = new HumanizedComparedWithTodayDurationPipe()
        })

        it("create an instance", () => {
            expect(pipe).toBeTruthy()
        })

        it("should humanize the duration between now and few seconds ago", () => {
            pastDate = moment()
                .add(-15, "seconds")
                .toDate()
            expect(pipe.transform(pastDate)).toBe("a few seconds ago")
        })
    })
})

Let’s write similar test cases for other durations:

// humanized-compared-with-today-duration.pipe.spec.ts

// ..
describe("HumanizedComparedWithTodayDurationPipe", () => {
    describe("Isolated HumanizedComparedWithTodayDurationPipe tests", () => {
        // ..
        it("should humanize the duration between now and few minutes ago", () => {
            pastDate = moment()
                .add(-23, "minutes")
                .toDate()
            expect(pipe.transform(pastDate)).toBe("23 minutes ago")
        })

        it("should humanize the duration between now and few hours ago", () => {
            pastDate = moment()
                .add(-8, "days")
                .toDate()
            expect(pipe.transform(pastDate)).toBe("8 days ago")
        })

        it("should humanize the duration between now and few days ago", () => {
            pastDate = moment()
                .add(-5, "days")
                .toDate()
            expect(pipe.transform(pastDate)).toBe("5 days ago")
        })

        it("should humanize the duration between now and few months ago", () => {
            pastDate = moment()
                .add(-4, "months")
                .toDate()
            expect(pipe.transform(pastDate)).toBe("4 months ago")
        })

        it("should humanize the duration between now and few years ago", () => {
            pastDate = moment()
                .add(-4, "years")
                .toDate()
            expect(pipe.transform(pastDate)).toBe("4 years ago")
        })
    })
})

Now we can run the tests - ng test. All tests are passed:

Karma

All Isolated tests passed

Shallow Pipe Tests

Alright, we’re done with isolated tests. Now let’s implement shallow tests. We’ll test how our pipe works with a real Angular component. It will require working with the TestBed.

Let’s add Component, ComponentFixture and TestBed to the import section:

// humanized-compared-with-today-duration.pipe.spec.ts

import { Component } from "@angular/core"
import { ComponentFixture, TestBed } from "@angular/core/testing"
import { HumanizedComparedWithTodayDurationPipe } from "./humanized-compared-with-today-duration.pipe"
import * as moment from "moment"

Then we have to initialize the TestBed. It can be done in a separate file, i.e. Angular CLI creates test.ts with the right settings:

// test.ts

import { getTestBed } from "@angular/core/testing"
import {
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting
} from "@angular/platform-browser-dynamic/testing"

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting()
)

The key thing here is that the TestBed should be initialized once in the project.

Now we’re ready to write our test suite of shallow tests. First of all, we need to create an auxiliary component employing the pipe and having the date property:

// humanized-compared-with-today-duration.pipe.spec.ts

describe("HumanizedComparedWithTodayDurationPipe", () => {
    describe("Shallow HumanizedComparedWithTodayDurationPipe tests", () => {
        @Component({
            template: `The event happened {{ date | humanizedComparedWithTodayDuration}}`
        })
        class EventHappenedComponent {
            date: Date = new Date()
        }
    })
})

Then we have to declare variables for the component, its fixture, its element and a pastDate which will bee different for each test case. Also, we configure the TestBed and assign values for the fixture, component and the element in the beforeEach() section to make our tests isolated from each other still having a common place for the initialization:

// humanized-compared-with-today-duration.pipe.spec.ts

// ...
describe("HumanizedComparedWithTodayDurationPipe", () => {
    describe("Shallow HumanizedComparedWithTodayDurationPipe tests", () => {
        // ...

        let component: EventHappenedComponent
        let fixture: ComponentFixture<EventHappenedComponent>
        let element: HTMLElement
        let pastDate

        beforeEach(() => {
            TestBed.configureTestingModule({
                declarations: [
                    HumanizedComparedWithTodayDurationPipe,
                    EventHappenedComponent
                ]
            })

            fixture = TestBed.createComponent(EventHappenedComponent)
            component = fixture.componentInstance
            element = fixture.nativeElement
        })
    })
})

Initialization part is complete. We’re ready to write test cases. The logic of the tests is similar to the logic of the initialization tests but we run in the Angular environment using the prerequisites: the component, it fixture, and htmlElement.

// humanized-compared-with-today-duration.pipe.spec.ts

// ...
describe("HumanizedComparedWithTodayDurationPipe", () => {
    describe("Shallow HumanizedComparedWithTodayDurationPipe tests", () => {
        // ...
        it("should humanize the duration between now and few seconds ago", () => {
            pastDate = moment()
                .add(-15, "seconds")
                .toDate()
            component.date = pastDate
            fixture.detectChanges() // This will call things like ngOnInit() and other events
            expect(element.textContent).toContain(
                "The event happened a few seconds ago"
            )
        })

        it("should humanize the duration between now and few minutes ago", () => {
            pastDate = moment()
                .add(-20, "minutes")
                .toDate()
            component.date = pastDate
            fixture.detectChanges() // This will call things like ngOnInit() and other events
            expect(element.textContent).toContain(
                "The event happened 20 minutes ago"
            )
        })

        it("should humanize the duration between now and few hours ago", () => {
            pastDate = moment()
                .add(-15, "hours")
                .toDate()
            component.date = pastDate
            fixture.detectChanges() // This will call things like ngOnInit() and other events
            expect(element.textContent).toContain(
                "The event happened 15 hours ago"
            )
        })

        it("should humanize the duration between now and few days ago", () => {
            pastDate = moment()
                .add(-10, "days")
                .toDate()
            component.date = pastDate
            fixture.detectChanges() // This will call things like ngOnInit() and other events
            expect(element.textContent).toContain(
                "The event happened 10 days ago"
            )
        })

        it("should humanize the duration between now and few months ago", () => {
            pastDate = moment()
                .add(-5, "months")
                .toDate()
            component.date = pastDate
            fixture.detectChanges() // This will call things like ngOnInit() and other events
            expect(element.textContent).toContain(
                "The event happened 5 months ago"
            )
        })

        it("should humanize the duration between now and few years ago", () => {
            pastDate = moment()
                .add(-3, "years")
                .toDate()
            component.date = pastDate
            fixture.detectChanges() // This will call things like ngOnInit() and other events
            expect(element.textContent).toContain(
                "The event happened 3 years ago"
            )
        })
    })
})

Now we can run the tests - ng test. All tests are passed:

Karma

All the tests passed

Summary

Since pipes are basically simple classes with a single method - transform, we can test them in the isolated mode. Another approach is to test their job with real components generated on the fly by means of the Angular environment - it’s shallow testing.

Feel free to check out the repository.

Happy coding!


Written by Egor Panok, full stack JavaScript Developer who loves building useful things. Follow him on Twitter

© 2020, Egor Panok, Full Stack JavaScript Developer