Final Thoughts

(To follow along, grab the project from Github and switch to the master branch).

In the upcoming sections, we will build a Calendar. This component will let users view the current month or jump to a specific date. As noted before, manually testing the Calendar is a tedious affair. We have to verify the current month has no duplicate or absent days, confirm the logic holds up when generating calendars for future dates, and ensure February is handled correctly in both leap and non-leap years. That is a lot of repetitive checks.

We will start by building the Calendar's header. You give it a date, and it returns something like September of 2021. That is, of course, a pipe.

Pipes are the simplest building blocks in Angular, and testing them is equally straightforward.

We are following a test-driven approach throughout this series, so let's open calendar.spec.ts and lay down a basic test structure.

File: libs/calendar/src/calendar.pipe.spec.ts:

import { CalendarPipe } from './calendar.pipe';

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

  beforeEach(() => {
    pipe = new CalendarPipe();
  });
});
Enter fullscreen mode Exit fullscreen mode

Here, we import our pipe and create an instance in the beforeEach hook. Now let's add a test:

File: libs/calendar/src/calendar.pipe.spec.ts:

import { CalendarPipe } from './calendar.pipe';

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

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

  it('transforms 2021/06 to "June of 2021"', () => {
    expect(pipe.transform('2021/06')).toBe('June of 2021');
  });
});
Enter fullscreen mode Exit fullscreen mode

This test effectively defines the pipe's public API. We pass in a date string and expect a human-readable English date string back.

Note: To run the tests, use: npm run test:all -- --watch so it executes everything and listens for changes.

As expected, this fails. Although the pipe class exists, it currently returns null.

first test fails

Let's implement it:

File: libs/calendar/src/calendar.pipe.ts:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'calendar',
})
export class CalendarPipe implements PipeTransform {
  transform(value: string): string {
    const dateParts = value.split('/');

    const date = new Date(+dateParts[0], +dateParts[1]);

    return `${date.toLocaleDateString('en-us', { month: 'long'})} of ${date.getFullYear()}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

We split the input date into two components, use them to instantiate a new Date object, and then build the output string. The test should pass now:

first test still fails

Wait, it doesn't.

...

Right, JavaScript months are zero-indexed. So passing 06 actually refers to July, not June. Since it's better to offer a user-friendly API, let's adjust our code so the API is not zero-based.

File: libs/calendar/src/calendar.pipe.ts:

transform(value: string): string {
  const dateParts = value.split('/');

  const date = new Date(+dateParts[0], +dateParts[1] - 1);

  return `${date.toLocaleDateString('en-us', { month: 'long'})} of ${date.getFullYear()}`;
}
Enter fullscreen mode Exit fullscreen mode

first test pass

That's better.

Fun fact: This API is far from ideal. Requiring the user to split a string, parse the pieces as numbers, and then construct a date object is unnecessarily complex. Throughout this course, we will see several times that this API is poorly designed. It's a good example that passing tests don't necessarily mean the code is well-designed or easy to use; they just confirm it behaves as intended.

Now that our first test is green, let's call the API with different inputs to check its behavior:

File: libs/calendar/src/calendar.pipe.ts:

it('transforms 2040/8 to "August of 2040"', () => {
  expect(pipe.transform('2040/8')).toBe('August of 2040');
});
Enter fullscreen mode Exit fullscreen mode

second test pass

Even without the leading zero, the output remains correct.

What if we supply a malformed date string?

File: libs/calendar/src/calendar.pipe.ts:

it('transforms 2021 to "Unknown date"', () => {
  expect(pipe.transform('2021')).toBe('Unknown Date');
});
Enter fullscreen mode Exit fullscreen mode

third test fail

Invalid Date of NaN — well, that's what I get when I try to input my vacation days. All jokes aside, let's fix that:

File: libs/calendar/src/calendar.pipe.ts:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'calendar'
})
export class CalendarPipe implements PipeTransform {
  transform(value: string): string {
    const dateParts = value.split('/');

    if (dateParts.length !== 2) { return 'Unknown Date'; }

    const date = new Date(+dateParts[0], +dateParts[1] - 1);

    return `${date.toLocaleDateString('en-us', { month: 'long' })} of ${date.getFullYear()}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

We simply check if the input string is malformed; if it is, we return an error message.

all test pass

Conclusions

Testing pipes is quite straightforward. It is no different from our Calculator example. You instantiate it, write a few test cases, and you're done.

Up next, we will implement the Calendar's core logic — the service.