Skip to main content

Angular Pipe Testing

Introduction

Pipes are one of Angular's most useful features, allowing you to transform data directly in your templates. Whether you're formatting dates, filtering arrays, or converting text to uppercase, pipes help keep your component logic clean while handling data transformations in the view.

Just like any other part of your Angular application, it's important to test pipes to ensure they work correctly. In this tutorial, we'll learn how to write comprehensive tests for Angular pipes, from basic to more complex scenarios.

Understanding Pipe Testing Fundamentals

Angular pipes are classes with a transform method that accepts an input value and optional parameters and returns the transformed value. Testing pipes is generally straightforward since they're typically pure functions (same input always produces same output) with minimal dependencies.

Why Test Pipes?

  • Verify correctness: Ensure pipes transform data as expected
  • Protect against regressions: Prevent future changes from breaking existing functionality
  • Document expected behavior: Tests serve as documentation for how pipes should work
  • Support refactoring: Allow you to refactor pipe implementation while maintaining behavior

Setting Up Your Testing Environment

Before we start writing tests, make sure you have the Angular testing utilities available in your project. If you created your project with the Angular CLI, these should already be set up.

The key imports you'll need for testing pipes include:

typescript
import { TestBed } from '@angular/core/testing';

Testing a Simple Pipe

Let's start by creating and testing a simple pipe that reverses a string.

First, let's create our pipe:

typescript
// reverse.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'reverse'
})
export class ReversePipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
return value.split('').reverse().join('');
}
}

Now let's write a test for this pipe:

typescript
// reverse.pipe.spec.ts
import { ReversePipe } from './reverse.pipe';

describe('ReversePipe', () => {
let pipe: ReversePipe;

beforeEach(() => {
pipe = new ReversePipe();
});

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

it('should reverse a string', () => {
expect(pipe.transform('hello')).toBe('olleh');
});

it('should handle empty string', () => {
expect(pipe.transform('')).toBe('');
});

it('should handle null input', () => {
expect(pipe.transform(null as any)).toBe('');
});
});

Key Points in the Test

  1. We create a new instance of the pipe in the beforeEach method
  2. We test basic pipe creation
  3. We test the main functionality (reversing a string)
  4. We test edge cases (empty string and null input)

Testing a Pipe with Parameters

Many pipes accept parameters that customize their behavior. Let's test a truncate pipe that shortens text and adds an ellipsis.

typescript
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'truncate'
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 20, completeWords: boolean = false, ellipsis: string = '...'): string {
if (!value) return '';

if (value.length <= limit) return value;

if (completeWords) {
limit = value.substring(0, limit).lastIndexOf(' ');
}

return `${value.substring(0, limit)}${ellipsis}`;
}
}

Now let's test it with different parameter combinations:

typescript
// truncate.pipe.spec.ts
import { TruncatePipe } from './truncate.pipe';

describe('TruncatePipe', () => {
let pipe: TruncatePipe;

beforeEach(() => {
pipe = new TruncatePipe();
});

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

it('should not change text shorter than the limit', () => {
expect(pipe.transform('Short text', 20)).toBe('Short text');
});

it('should truncate text longer than the limit', () => {
expect(pipe.transform('This is a long text that should be truncated', 20))
.toBe('This is a long text ...');
});

it('should respect the completeWords parameter', () => {
expect(pipe.transform('This is a long text that should be truncated', 20, true))
.toBe('This is a long ...');
});

it('should use custom ellipsis', () => {
expect(pipe.transform('This is a long text that should be truncated', 20, false, '---'))
.toBe('This is a long text ---');
});

it('should handle all parameters together', () => {
expect(pipe.transform('This is a long text that should be truncated', 20, true, '~~~'))
.toBe('This is a long ~~~');
});
});

Testing Pure vs. Impure Pipes

Pure pipes are only executed when Angular detects a pure change to the input value (a change to a primitive value or a new object reference). Impure pipes are executed during every change detection cycle.

Let's test a simple impure pipe that includes the current timestamp:

typescript
// timestamp.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'timestamp',
pure: false
})
export class TimestampPipe implements PipeTransform {
transform(value: any): string {
const now = new Date();
return `${value} [${now.toLocaleTimeString()}]`;
}
}

Testing impure pipes can be tricky because they may depend on external state or produce different results on each call. We can use spies to control this behavior:

typescript
// timestamp.pipe.spec.ts
import { TimestampPipe } from './timestamp.pipe';

describe('TimestampPipe', () => {
let pipe: TimestampPipe;

beforeEach(() => {
pipe = new TimestampPipe();
});

it('should create an instance', () => {
expect(pipe).toBeTruthy();
});

it('should append the current timestamp to the value', () => {
// Setup a fake date
const fakeDate = new Date('2023-01-01T12:00:00');
spyOn(global, 'Date').and.returnValue(fakeDate);

const result = pipe.transform('Hello');
expect(result).toBe('Hello [12:00:00 PM]');
});
});

Testing Built-in Pipes

Sometimes you might want to test how your component interacts with Angular's built-in pipes. Let's see how to test a component using the DatePipe:

First, the component:

typescript
// date-display.component.ts
import { Component, Input } from '@angular/core';
import { DatePipe } from '@angular/common';

@Component({
selector: 'app-date-display',
template: `<div>Date: {{ date | date:'shortDate' }}</div>`,
providers: [DatePipe]
})
export class DateDisplayComponent {
@Input() date: Date;
}

Now the test:

typescript
// date-display.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DateDisplayComponent } from './date-display.component';
import { DatePipe } from '@angular/common';

describe('DateDisplayComponent', () => {
let component: DateDisplayComponent;
let fixture: ComponentFixture<DateDisplayComponent>;
let datePipe: DatePipe;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DateDisplayComponent],
providers: [DatePipe]
}).compileComponents();

fixture = TestBed.createComponent(DateDisplayComponent);
component = fixture.componentInstance;
datePipe = TestBed.inject(DatePipe);
});

it('should display formatted date', () => {
const testDate = new Date('2023-01-15');
component.date = testDate;

fixture.detectChanges();

const expectedText = `Date: ${datePipe.transform(testDate, 'shortDate')}`;
expect(fixture.nativeElement.textContent).toContain(expectedText);
});
});

Real-world Example: A Filter Pipe

Let's examine a more practical example of a filter pipe that might be used in a real application:

typescript
// filter-products.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

interface Product {
id: number;
name: string;
category: string;
price: number;
inStock: boolean;
}

@Pipe({
name: 'filterProducts'
})
export class FilterProductsPipe implements PipeTransform {
transform(products: Product[], searchTerm: string = '', onlyInStock: boolean = false): Product[] {
if (!products) return [];

let result = products;

// Filter by search term
if (searchTerm) {
searchTerm = searchTerm.toLowerCase();
result = result.filter(product =>
product.name.toLowerCase().includes(searchTerm) ||
product.category.toLowerCase().includes(searchTerm)
);
}

// Filter by stock
if (onlyInStock) {
result = result.filter(product => product.inStock);
}

return result;
}
}

Here's how we would test this pipe:

typescript
// filter-products.pipe.spec.ts
import { FilterProductsPipe } from './filter-products.pipe';

describe('FilterProductsPipe', () => {
let pipe: FilterProductsPipe;
let products: any[];

beforeEach(() => {
pipe = new FilterProductsPipe();
products = [
{ id: 1, name: 'Laptop', category: 'Electronics', price: 999.99, inStock: true },
{ id: 2, name: 'Phone', category: 'Electronics', price: 699.99, inStock: false },
{ id: 3, name: 'Desk Chair', category: 'Furniture', price: 149.99, inStock: true },
{ id: 4, name: 'Coffee Mug', category: 'Kitchen', price: 9.99, inStock: true }
];
});

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

it('should return all products when no filters applied', () => {
const result = pipe.transform(products);
expect(result.length).toBe(4);
});

it('should filter products by name search term', () => {
const result = pipe.transform(products, 'lap');
expect(result.length).toBe(1);
expect(result[0].name).toBe('Laptop');
});

it('should filter products by category search term', () => {
const result = pipe.transform(products, 'electronics');
expect(result.length).toBe(2);
});

it('should filter out of stock products when onlyInStock is true', () => {
const result = pipe.transform(products, '', true);
expect(result.length).toBe(3);
expect(result.some(p => p.name === 'Phone')).toBeFalsy();
});

it('should combine filters correctly', () => {
const result = pipe.transform(products, 'electronics', true);
expect(result.length).toBe(1);
expect(result[0].name).toBe('Laptop');
});

it('should handle null input', () => {
const result = pipe.transform(null as any);
expect(result).toEqual([]);
});
});

Testing Pipes with Dependencies

Sometimes pipes may have dependencies injected into them. Let's create a translation pipe that depends on a translation service:

typescript
// translate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { TranslationService } from './translation.service';

@Pipe({
name: 'translate'
})
export class TranslatePipe implements PipeTransform {
constructor(private translationService: TranslationService) {}

transform(key: string, params: object = {}): string {
return this.translationService.translate(key, params);
}
}

To test this pipe, we'll need to mock the translation service:

typescript
// translate.pipe.spec.ts
import { TranslatePipe } from './translate.pipe';
import { TranslationService } from './translation.service';

describe('TranslatePipe', () => {
let pipe: TranslatePipe;
let mockTranslationService: jasmine.SpyObj<TranslationService>;

beforeEach(() => {
mockTranslationService = jasmine.createSpyObj('TranslationService', ['translate']);
pipe = new TranslatePipe(mockTranslationService);
});

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

it('should call translation service with the provided key', () => {
mockTranslationService.translate.and.returnValue('Translated Text');

const result = pipe.transform('HELLO');

expect(mockTranslationService.translate).toHaveBeenCalledWith('HELLO', {});
expect(result).toBe('Translated Text');
});

it('should pass parameters to the translation service', () => {
const params = { name: 'John' };
mockTranslationService.translate.and.returnValue('Hello, John');

const result = pipe.transform('GREETING', params);

expect(mockTranslationService.translate).toHaveBeenCalledWith('GREETING', params);
expect(result).toBe('Hello, John');
});
});

Best Practices for Pipe Testing

  1. Test edge cases: null values, empty strings, boundary conditions
  2. Test all parameters: If your pipe accepts multiple parameters, test various combinations
  3. Mock dependencies: Use Jasmine spies or mocks for external dependencies
  4. Keep tests simple: Each test should focus on a single aspect of the pipe's behavior
  5. Test pure function nature: For pure pipes, verify that the same input always produces the same output
  6. Test performance: For expensive pipes, consider testing performance constraints

Common Mistakes to Avoid

  1. Not testing null or undefined inputs: Always test how your pipe handles these cases
  2. Ignoring error cases: Test how your pipe behaves when given invalid inputs
  3. Only testing happy paths: Make sure to include edge cases
  4. Testing implementation details: Focus on testing the pipe's output for given inputs, not internal methods
  5. Overlooking date/time dependencies: Use mocking for date/time operations to ensure consistent tests

Summary

In this tutorial, we've covered:

  • The basics of setting up pipe tests
  • Testing simple pipes without parameters
  • Testing pipes with various parameter combinations
  • Testing pure vs. impure pipes
  • Testing built-in pipes
  • Testing pipes with dependencies
  • Best practices and common mistakes

Testing pipes is generally straightforward, but thorough testing ensures your data transformations work correctly and remain reliable as your application evolves.

Additional Resources

Exercises

  1. Create a FileSizePipe that formats byte sizes to KB, MB, GB etc. and write comprehensive tests for it.
  2. Write tests for an HighlightSearchPipe that wraps matching text in HTML span tags.
  3. Create a SortByPipe that sorts an array of objects by a specified property and write tests for ascending and descending order.
  4. Implement tests for an impure RandomPipe that adds a random number between 1-10 to the input.
  5. Create and test a MarkdownPipe that converts markdown text to HTML (this would involve a dependency on a markdown library).


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)