5
Testing pipes

This chapter covers

  • Testing pipes
  • Understanding pure functions versus functions with side effects
  • Using the transform method

Often, you’ll want to modify data that’s displayed in a template. For example, you may want to format a number as currency, transform a date into a format that’s easier to understand, or make some text uppercase. In situations like these, Angular provides a way to transform data using something known as a pipe.

Pipes take input, transform it, and then return some transformed value. Because the way pipes operate is straightforward, writing tests for them is too. Pipes depend only on their input. A function whose output depends on only the input passed to it is known as a pure function.

When a function can do something other than return a value, it’s said to have a side effect. A side effect could be changing a global variable or making an HTTP call. Pure functions like pipes don’t have side effects, which is why they’re easy to test.

In this chapter, we’ll cover everything you need to know to test pipes.

5.1 Introducing PhoneNumberPipe

In this chapter, you’ll be testing a custom pipe called PhoneNumberPipe. This pipe takes in a phone number as a number or string in valid format and puts it into a format that the user specifies. You need to write tests for the pipe so you can confirm that it transforms data into the right format.

Each pipe in Angular has a method named transform. This method is responsible for formatting the pipe’s input. The signature for the transform function for PhoneNumberPipe looks like this:

transform(value: string, format?: string, countryCode?: string): string

value is passed into the function from the left of the pipe and represents a phone number. format is an optional string parameter that determines how the phone number is formatted. Different valid values for format are listed in table 5.1.

Table 5.1 Recognized format values
Number separator formatPhone number format
default(XXX) XXX-XXXX
dotsXXX.XXX.XXXX
hyphensXXX-XXX-XXXX

countryCode is another optional string parameter that adds a prefix to the phone number as an international country code. For example, if you pass in a countryCode of 'us' (for the United States) and a format 'default', the resulting phone number would be +1 (XXX) XXX-XXXX.

To keep it simple, PhoneNumberPipe only works with phone numbers that follow the North American Numbering Plan (NANP), so the country codes you can use are limited to the countries in the NANP. If you’re curious about the acceptable country codes, look at the country-dialing-codes.ts file. An object there contains the two-character country abbreviation as a key and the international country code as the value.

Now that you know a bit about PhoneNumberPipe, you can test it like so:

  1. Set up the test dependencies.
  2. Test the default behavior.
  3. Test the format parameter.
  4. Finally, test the countryCode parameter.

You’ll continue with testing the Contacts app, as you’ve done in previous chapters. If you need to set it up, follow the instructions in appendix A.

5.2 Testing PhoneNumberPipe

Open website/src/app/contacts/shared/phone-number, and you should see the files described in table 5.2.

Table 5.2 Description of files
FileDescription
index.tsYou use the index.ts file so that you can import PhoneNumberPipe without using the complete file name. That way, when you’re trying to import PhoneNumberPipe, you can use
import { PhoneNumberPipe } from './phone-number.pipe';
instead of the more verbose
import { PhoneNumberPipe } from './phone-number.pipe/phone-number.pipe';
Notice the addition to the file name in bold. Using an index.ts file like this is a common practice to shorten file paths.
country-dialing-codes.tsThis file contains the country dialing codes that your PhoneNumber model uses.
phone-number-error-messages.tsThis file contains all the error messages that PhoneNumberPipe and the PhoneNumber model use.
phone-number.model.tsThis is the model that you’ll use to store data. The PhoneNumber model also contains the utility methods to transform the data.
phone-number.pipe.tsThis is the file that contains PhoneNumberPipe.

Feel free to open these files to get a feel for the source code you’ll be testing. When you’re ready to move on, create a file named phone-number.pipe.spec.ts in the phone-number directory to store your tests.

5.2.1 Testing the default usage for a pipe

Start by testing the default behavior of PhoneNumberPipe. Here’s an example of the default usage of PhoneNumberPipe:

{{ 7035550123 | phoneNumber }}

You need to test two different cases of the default usage of PhoneNumberPipe, as listed in table 5.3.

Table 5.3 Default test cases
Test caseNumberDisplays
A phone number that’s a 10-character string or 10-digit number should transform to the (XXX) XXX-XXXX format.7035550123(703) 555-0123
Nothing will be displayed when a phone number isn’t a 10-character string or 10-digit number.703555012

Testing for a valid phone number

Start by testing the default usage to see if the phone number is valid. Copy the code for the first default test in the following listing into the phone-number.pipe.spec.ts file that you just created.

Listing 5.1 First default test case

import { PhoneNumberPipe } from './phone-number.pipe';    ①  

describe('PhoneNumberPipe Tests', () => {    ②  
  let phoneNumber: PhoneNumberPipe = null;

  beforeEach(() => {    ③  
    phoneNumber = new PhoneNumberPipe();
  });

  describe('default behavior', () => {    ④  
    it('should transform the string or number into the default phone
      format', () => {
      const testInputPhoneNumber = '7035550123';
      const transformedPhoneNumber =
        phoneNumber.transform(testInputPhoneNumber);
      const expectedResult = '(703) 555-0123';

      expect(transformedPhoneNumber).toBe(expectedResult);    ⑤  
    });
  });

  afterEach(() => {    ⑥  
    phoneNumber = null;
  });
});

Let’s break this down by section:

import { PhoneNumberPipe } from './phone-number.pipe';

First, you import all of the dependencies that your test needs. Because the pipe is a pure function, you don’t need any of the Angular testing dependencies:

describe('PhoneNumberPipe Tests', () => {

});

You then add a describe function to house all your tests for PhoneNumberPipe:

let phoneNumber: PhoneNumberPipe = null;

beforeEach(() => {
  phoneNumber = new PhoneNumberPipe();
});

Inside your test suite, you need to create a global variable named phoneNumber that has a type of PhoneNumberPipe and is set to null. You use a beforeEach function to create a new instance of PhoneNumberPipe before each test is executed:

describe('default behavior', () => {
  it('should transform the string or number into the default phone format',
    () => {
    const testInputPhoneNumber = '7035550123';
    const transformedPhoneNumber =
      phoneNumber.transform(testInputPhoneNumber);
    const expectedResult = '(703) 555-0123';

    expect(transformedPhoneNumber).toBe(expectedResult);
  }));
});

This describe block defines the nested test suite that contains your tests for default behavior. You declare your test input in the testInputPhoneNumber variable, save the transformed result in transformedPhoneNumber, and set your expected result in expectedResult. The assertion at the bottom of the test checks that the transformed phone number matches your expected result:

afterEach(() => {
    phoneNumber = null;
});

Finally, the afterEach function makes sure the phoneNumber variable doesn’t contain a reference to an instance of PhoneNumberPipe. Run npm test, and you should see output like figure 5.1.

c05_01.png

Figure 5.1 First successfully executed pipe test

That’s it for your first test. The tests in the rest of the chapter follow the same format as the first one:

describe(describe a suite of tests, () => {
  it(describe the specific test case, () => {
    declare your test variables
    transform the data
    expect(the transformed data).toBe(what you expect);
  });
});

Tests for pipes all follow this structure because, as mentioned before, pipes are pure functions. There’s no need to mock or set anything up—you pass the function some input and confirm the result is what you’d expect.

Testing the pipe with an invalid phone number

For the second test, you’ll verify that if the input number doesn’t have 10 digits, nothing will be shown. Copy the code in the it block that you created previously and paste it directly after your first test.

Change the descriptive text in the it block to 'should not display anything if the length is not 10 digits'. Then change testInputPhoneNumber to '703555012'. Notice that the new phone number is only nine digits long. Now, set expectedResult to ''. You expect the result to be an empty string because that’s what should be returned if the phone number is invalid.

The completed test should look like the following listing.

Listing 5.2 Test for invalid phone number

it('should not display anything if the length is not 10 digits', 
  () => {    ①  
  const testInputPhoneNumber = '703555012';    ②  
  const transformedPhoneNumber =
    phoneNumber.transform(testInputPhoneNumber);
  const expectedResult = '';    ③  

  expect(transformedPhoneNumber).toBe(expectedResult);
});

If you run ng test, you’ll see something like figure 5.2.

c05_02.png

Figure 5.2 Two passing default behavior tests with an error message

Notice that the error message 'The phone number you have entered is not the proper length. It should be 10 characters long.' is printed out to the console along with the successful test execution messages. This is expected because PhoneNumberPipe throws an error message if the phone number is not 10 characters long. When you add console logging statements to testing using the Angular CLI default setting, they will be printed out to the terminal when tests run, as shown in figure 5.2.

Now that you’ve tested the default behavior, let’s look at testing a pipe with a single parameter.

5.2.2 Testing a pipe with a single parameter

Sometimes, you’ll need to change the behavior of a pipe by passing it a parameter. For example, you can change the format of the output of PhoneNumberPipe by passing 'dots', 'hyphens', or 'default' as a parameter.

Table 5.4 shows the different options for the format parameter.

Table 5.4 Test cases for the format parameter
Test caseFormatNumberDisplays
If 'default' is used or no parameter is specified, then the number will be in the default (XXX) XXX-XXXX format.default7035550123(703) 555-0123
If 'dots' is passed in as a parameter, then the number should be in XXX.XXX.XXXX format.dots7035550123703.555.0123
If 'hyphens' is passed in as a parameter, then the number should be in XXX-XXX-XXXX format.hyphens7035550123703-555-0123
If an unrecognized format is passed in as a parameter, then the default (XXX) XXX-XXXX format should be used.gibberish7035550123(703) 555-0123

Here’s an example usage of PhoneNumberPipe with a single parameter:

{{ 7035550123 | phoneNumber:'dots' }}

In this example, you pass 'dots' as a parameter.

Let’s look at some tests for when you use a single parameter for a pipe. Add the code in the following listing directly after the describe block that you created in listing 5.1.

Listing 5.3 'dots' format test

describe('phone number format tests', () => {    ①  
  it('should format the phone number using the dots format', () => {
    const testInputPhoneNumber = '7035550123';
    const format = 'dots';    ②  
    const transformedPhoneNumber =
      phoneNumber.transform(testInputPhoneNumber, format);    ③  
    const expectedResult = '703.555.0123';

    expect(transformedPhoneNumber).toBe(expectedResult);
  });
});

First off, notice that you’ve put this test inside a test suite using a describe block. On the fourth line of the code, you have a constant named format that you’ve set to 'dots'. On the fifth line of the code, you pass that format variable in as a second parameter in your transform method. You test for the first parameter that a pipe uses by sending the first parameter into your transform method as the second parameter.

Run ng test, and your output should look like figure 5.3.

c05_03.png

Figure 5.3 Three passing tests with an error message

Now that you understand how to test the first parameter, it's time for a little exercise.

Exercise

After the first parameter test that you added in listing 5.3, create the three tests for the 'default', 'hyphens', and 'gibberish' formats using the information provided in table 5.4.

Solution

All your tests should be similar. The only difference should be the new format type and the expected result based on that format type. Your three new tests should look like the following listing.

Listing 5.4 Remaining format tests

it('should format the phone number using the default format', () => {
  const testInputPhoneNumber = '7035550123';
  const format = 'default';    ①  
  const transformedPhoneNumber =
    phoneNumber.transform(testInputPhoneNumber, format);
  const expectedResult = '(703) 555-0123';    ②  

  expect(transformedPhoneNumber).toBe(expectedResult);
});


it('should format the phone number using the hyphens format', () => {
  const testInputPhoneNumber = '7035550123';
  const format = 'hyphens';    ①  
  const transformedPhoneNumber =
    phoneNumber.transform(testInputPhoneNumber, format);
  const expectedResult = '703-555-0123';    ②  

  expect(transformedPhoneNumber).toBe(expectedResult);
});

it('should format the phone number using the default format if unrecognized
  format is entered',() => {
  const testInputPhoneNumber = '7035550123';
  const format = 'gibberish';    ①  
  const transformedPhoneNumber =
    phoneNumber.transform(testInputPhoneNumber, format);
  const expectedResult = '(703) 555-0123';    ②  

  expect(transformedPhoneNumber).toBe(expectedResult);
});

Run ng test, and now you should see six passing tests. Now that you have one-parameter tests under control, let’s take a look at how to test multiple parameters.

5.2.3 Pipes with multiple parameters

Pipes can take multiple parameters if needed. PhoneNumberPipe can handle two parameters. So far, we’ve covered the first parameter and how it’s responsible for formatting the phone number. The second parameter is the country code. Table 5.5 shows the test cases for the country code parameter.

Table 5.5 Test cases for country code parameter
Test caseNumberCountry codeDisplays
If 'dots' is passed in as a parameter and the country code is correct, then the number should be in XXX.XXX.XXXX format with a plus sign and the country code before it.7035550123us+ 1 703.555.0123
If 'dots' is passed in as a parameter and an unrecognized country code is passed in, then the number should be in XXX.XXX.XXXX format with no country code applied.7035550123zz703.555.0123

For simplicity, PhoneNumberPipe only supports countries in the NANP. You need to test to make sure that each parameter is accepted and works as expected. Add the code in the following listing directly after the describe block that you created earlier that contains the phone number format tests.

Listing 5.5 Country code test

describe('country code parameter tests', () => {
  it('should add respective country code', () => {
    const testInputPhoneNumber = '7035550123';
    const format = 'default';
    const countryCode = 'us';    ①  
    const transformedPhoneNumber =
      phoneNumber.transform(testInputPhoneNumber, format, countryCode);    ②  
    const expectedResult = '+1 (703) 555-0123';    ③  

    expect(transformedPhoneNumber).toBe(expectedResult);
  });
});

This test is similar to the earlier tests for passing the first parameter to the pipe. The only difference is that earlier you were testing the second parameter, whereas now you’re passing a third parameter to your transform method. You may be picking up on a pattern. If you want to test a fourth pipe parameter, then you’d pass a value into the fifth parameter in your transform method. This pattern will continue for as many pipe parameters as you want to test.

Exercise

Write a test case such that when the country code is not recognized, PhoneNumberPipe only transforms the phone number format and doesn’t add a telephone country code. Make sure you run ng test to see if your test works as expected:

  • First hint —You can copy the listing 5.5 test and make modifications as necessary.
  • Second hint —The country code should be for a country code not listed in the NANP (http://mng.bz/R55f).

solution

You need to change only two variables. Change countryCode to something that’s unrecognized, and then change expectedResult to the default format with no country code prefixed to the phone number. Your test should look something like the following listing, which shows the changes in bold.

Listing 5.6 Test for invalid country code

it('should not add anything if the country code is unrecognized', () => {
  const testInputPhoneNumber = '7035550123';
  const format = 'default';
  const countryCode = 'zz';    ①  
  const transformedPhoneNumber =
    phoneNumber.transform(testInputPhoneNumber, format, countryCode);
  const expectedResult = '(703) 555-0123';    ②  

  expect(transformedPhoneNumber).toBe(expectedResult);
});

Run ng test, and you now should see eight passing tests. If you have any issues, check out the complete test at https://github.com/testing-angular-applications/testing-angular-applications/blob/master/chapter05/phone-number.pipe.spec.ts and look for any discrepancies. In the next chapter, we’ll start looking at testing services.

Summary

  • Because pipes only take in a value as input, transform that value, and then return transformed input, testing them is straightforward. That’s because they’re pure functions, which means they have no side effects.
  • Side effects are changes that occur outside a function after that function is executed. A common side effect is the changing of a global variable.
  • When you’re testing pipes, you’re mainly testing the transform method that’s included in every pipe. The transform method is what takes in the different parameters you want to manipulate, performs the manipulation, and then returns the changed values.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.152.99