Test Angular Pipes With Services

How to test an Angular pipe which uses injected services

Mar 25, 2020

#angular #testing #javascript #webdev

Photo by Guillaume TECHER on Unsplash

I share one trick a day until the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Twenty-five days left until hopefully better days.


Today I spent much time deep focused at writing new Angular components and their related unit tests, that I even missed this morning online “stand-up” and almost feel like I spend my day in some kind of vortex.

Anyway, I like this challenge, I don’t want to skip today’s blog post and I would like to share with you how I tested a new pipe I created. Moreover, I don’t pretend to be the champion of the exercise, therefore, if you notice anything which can be enhanced, ping me with your comments, I would be happy to improve my skills 🙏.


Create A Pipe

Let’s first create a blank pipe called “filter” with the ng command line.

ng g pipe filter

This creates a blank pipe like the following:

import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'filter' }) export class FilterPipe implements PipeTransform { transform(value: any, ...args: any[]): any { return null; } }

And it’s related test:

import { FilterPipe } from "./filter.pipe"; describe("FilterPipe", () => { it("create an instance", () => { const pipe = new FilterPipe(); expect(pipe).toBeTruthy(); }); });

You can be or not an Angular fan but I think we can all be agree that it’s pretty cool to have a CLI which creates class and related test without any effort.


Create A Service

As staten in my opening, the goal is to test a pipe which uses an injected service.

ng g service translation

For demo purpose, we create this dummy service “translation” wich return not that much except either “Génial” or “Awesome” as an observable.

import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class TranslationService { translate(lang: string): Observable<string> { return of(lang === 'fr' ? 'Génial' : 'Awesome'); } }

Implement Pipe

Our service being ready, we use it to enhance our pipe.

import { Pipe, PipeTransform } from '@angular/core'; import { TranslationService } from './translation.service'; import { Observable } from 'rxjs'; @Pipe({ name: 'filter' }) export class FilterPipe implements PipeTransform { constructor(private translationService: TranslationService) {} transform(lang: string): Observable<string> { return this.translationService.translate(lang); } }

Which by the way can be use with the help of the async pipe in a template (in following example, lang is a public string variable of the component)

<textarea [value]="lang | filter | async"></textarea>

Update Pipe Test

Locally I’m still able to run my test without errors but, because we are now injecting a service in our pipe, if we open the related unit test we notice a TypeScript error on the constructor TS2554: expected 1 arguments, but got 0 . To fix this we have now to either inject the service or mock it.


Resolving Service In Test

You can either resolve the service via the inject function or TestBed . Because the first solution didn’t worked out for me, the second one was my fallback.

import { FilterPipe } from './filter.pipe'; import { TestBed } from '@angular/core/testing'; import { TranslationService } from './translation.service'; describe('FilterPipe', () => { beforeEach(() => { TestBed .configureTestingModule({ providers: [ TranslationService ] }); }); it('create an instance', () => { const service: TranslationService = TestBed.get(TranslationService); const pipe = new FilterPipe(service); expect(pipe).toBeTruthy(); }); });

Mock Service

Another solution, the one I actually finally applied, is creating a mock of the service instead of providing it.

import { FilterPipe } from './filter.pipe'; import { of } from 'rxjs'; import { TranslationService } from './translation.service'; describe('FilterPipe', () => { let translationServiceMock: TranslationService; beforeEach(() => { translationServiceMock = { translate: jest.fn((lang: string) => of('Awesome')) } as any; }); it('create an instance', () => { const pipe = new FilterPipe(translationServiceMock); expect(pipe).toBeTruthy(); }); });

Test Pipe Transform

So far we were able to test that our pipe can be created even if it relies on a service but we are still not effectively testing its outcome. Therefore, here is the final piece, in which I use the mock of the service. Basically, once the pipe is created, we can access its transform method and proceed with some common testing.

import { FilterPipe } from './filter.pipe'; import { of } from 'rxjs'; import { take } from 'rxjs/operators'; import { TranslationService } from './translation.service'; describe('FilterPipe', () => { let translationServiceMock: TranslationService; beforeEach(() => { translationServiceMock = { translate: jest.fn((lang: string) => of('Awesome')) } as any; }); it('create an instance', () => { const pipe = new FilterPipe(translationServiceMock); expect(pipe).toBeTruthy(); }); it('should translate', () => { const pipe = new FilterPipe(translationServiceMock); pipe.transform('en') .pipe(take(1)) .subscribe((text: string) => { expect(text).not.toBe(null); expect(text).toEqual('Awesome'); }); }); });

Summary

It still takes me a bit to find the right testing setting for projects, specially when they are new, but as soon as everything is in place as soon as I can access the nativeElement to perform queries, as I would do in a Web Component, I feel more comfortable and it began to be fun 😁.

Stay home, stay safe!

David