# 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


```typescript
// 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:

```typescript
// 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


```typescript
// 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:

```typescript
// 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:

```html
<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


```typescript
beforeEach(() => {
  component.title = input;
  fixture.detectChanges();
});
```

* test the `title` element


```typescript
it(`should display "${ expectedText }"`, () => {
  expect(title().nativeElement.textContent).toBe(expected);
});
```

```typescript
// 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.

```text
 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

https://gitlab.com/barcioch-blog-examples/series-angular-testing-05-angular-pipe-testing


