Angular pipe testing

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 values

  • AppTestComponent - 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.

Source code

gitlab.com/barcioch-blog-examples/series-an..