Angular pipe testing
Table of contents
In this article, I'll show how to test Angular pipes. It can be done in two different ways:
directly create pipe and test output
use the pipe in the component and test HTML
In the example there will be created two simple pipes:
trim - for trimming strings
ucFirst - for changing the first character to uppercase
The environment is initialized with the following libraries:
Angular 15
Jest 29
The pipes
TrimPipe
The pipe works as follows:
take the input string
if it's null or undefined or an empty string - return an empty string
otherwise - return trimmed value
// trim.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'appTrim',
})
export class TrimPipe implements PipeTransform {
transform(input: string | null | undefined): string {
if (input == null || input.length === 0) {
return '';
}
return input.trim();
}
}
Test it:
// trim.pipe.spec.ts
import { TrimPipe } from './trim.pipe';
describe('TrimPipe', () => {
describe('when pipe is created', () => {
let pipe: TrimPipe;
beforeAll(() => {
pipe = new TrimPipe();
});
it.each([
{input: undefined, text: 'undefined'},
{input: null, text: 'null'},
{input: '', text: 'empty string'},
{input: '\n \n', text: '"\\n \\n"'},
])('should transform $text to empty string', ({input, text}) => {
expect(pipe.transform(input)).toBe('');
});
it.each([
{input: ' text', text: 'text', expectedText: 'text', expected: 'text'},
{input: 'text ', text: 'text', expectedText: 'text', expected: 'text'},
{input: 'text text', text: 'text text', expectedText: 'text text', expected: 'text text'},
{input: ' text text ', text: 'text text', expectedText: 'text text', expected: 'text text'},
{input: ' text \n text ', text: 'text \\n text', expectedText: 'text \\n text', expected: 'text \n text'},
{
input: '\n text \n text \n',
text: '\\n text \\n text \\n',
expectedText: 'text \\n text',
expected: 'text \n text'
},
])('should transform "$text" to "$expectedText"', ({input, expected, expectedText}) => {
expect(pipe.transform(input)).toBe(expected);
});
});
});
UcFirstPipe
The pipe works as follows:
take the input string
if it's null or undefined or an empty string - return an empty string
otherwise - make the first letter upper case and return the string
// uc-first.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'appUcFirst',
})
export class UcFirstPipe implements PipeTransform {
transform(input: string | null | undefined): string {
if (input == null || input.length === 0) {
return '';
}
return input[0].toUpperCase() + input.substring(1, input.length);
}
}
Test it:
// uc-first.pipe.spec.ts
import { UcFirstPipe } from './uc-first.pipe';
describe('UcFirstPipe', () => {
describe('when pipe is created', () => {
let pipe: UcFirstPipe;
beforeAll(() => {
pipe = new UcFirstPipe();
});
it.each([
{input: undefined, text: 'undefined'},
{input: null, text: 'null'},
{input: '', text: 'empty string'},
])('should transform $text to empty string', ({input, text}) => {
expect(pipe.transform(input)).toBe('');
});
it.each([
{input: ' ', expected: ' '},
{input: ' text', expected: ' text'},
{input: 'text', expected: 'Text'},
{input: 'Text', expected: 'Text'},
{input: 'a', expected: 'A'},
{input: '1a', expected: '1a'},
{input: 'TEXT ', expected: 'TEXT '},
])('should transform "$input" to "$expected"', ({input, expected}) => {
expect(pipe.transform(input)).toBe(expected);
});
});
});
Testing in component
To properly test components without worrying about change detection it's a good practice to wrap them in the wrapper component
and update only the wrapper's
values. An example, of how the final HTML structure will look like:
<app-wrapper>
<app-test>
<span class="result">{{ title | appTrim | appUcFirst}}</span>
</app-test>
</app-wrapper>
The test suite contains two extra definitions:
AppWrapperComponent
- wrapper component for setting valuesAppTestComponent
- the right component that uses the pipe and takes data by input. Usually, these kinds of components don't need to be defined inside tests since we want to use already existing components or modules.
The test procedure is quite straightforward:
- create testing module
TestBed.configureTestingModule()
- prepare test data using
describe.each()
- pass the data to the component and run change detection
beforeEach(() => {
component.title = input;
fixture.detectChanges();
});
- test the
title
element
it(`should display "${ expectedText }"`, () => {
expect(title().nativeElement.textContent).toBe(expected);
});
// pipes.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TrimPipe } from './pipes/trim.pipe';
import { UcFirstPipe } from './pipes/uc-first.pipe';
import { Component, DebugElement, Input } from '@angular/core';
import { By } from '@angular/platform-browser';
describe('Test Pipes', () => {
let fixture: ComponentFixture<AppWrapperComponent>;
let component: AppWrapperComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
TrimPipe,
UcFirstPipe,
AppTestComponent,
AppWrapperComponent
],
}).compileComponents();
fixture = TestBed.createComponent(AppWrapperComponent);
component = fixture.componentInstance;
});
describe.each([
{input: '', inputText: '', expected: '', expectedText: ''},
{input: 'article', inputText: 'article', expected: 'Article', expectedText: 'Article'},
{input: ' article ', inputText: ' article ', expected: 'Article', expectedText: 'Article'},
{input: ' new article ', inputText: ' new article ', expected: 'New article', expectedText: 'New article'},
{
input: ' \n new article \n ',
inputText: ' \\n new article \\n ',
expected: 'New article',
expectedText: 'New article'
},
{
input: ' \n new \n article \n ',
inputText: ' \\n new \\n article \\n ',
expected: 'New \n article',
expectedText: 'New \\n article'
},
])('when "$inputText" is passed', ({input, inputText, expected, expectedText}) => {
beforeEach(() => {
component.title = input;
fixture.detectChanges();
});
it(`should display "${ expectedText }"`, () => {
expect(title().nativeElement.textContent).toBe(expected);
});
});
const title = (): DebugElement => {
return fixture.debugElement.query(By.css('.title'));
}
});
@Component({
selector: 'app-test',
template: '<span class="title">{{ title | appTrim | appUcFirst }}</span>',
})
export class AppTestComponent {
@Input() title: string | null | undefined;
}
@Component({
selector: 'app-wrapper',
template: '<app-test [title]="title"></app-test>',
})
export class AppWrapperComponent {
title: string | null | undefined;
}
Run npx jest --coverage
and make sure that all the code paths you're interested in were tested.
PASS src/app/pipes/trim.pipe.spec.ts
PASS src/app/pipes/uc-first.pipe.spec.ts
PASS src/app/pipes.spec.ts
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
trim.pipe.ts | 100 | 100 | 100 | 100 |
uc-first.pipe.ts | 100 | 100 | 100 | 100 |
------------------|---------|----------|---------|---------|-------------------
Test Suites: 3 passed, 3 total
Tests: 26 passed, 26 total
Snapshots: 0 total
Time: 1.945 s, estimated 3 s
Ran all test suites.
Summary
The pipes are implemented and thoroughly tested. In this example, the pipes were defined directly in the testing module. In the real application, the pipe will be probably defined in a separate module that you're going to import. As always, try to cover as many cases as you can imagine.