transform
methodOften, 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.
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.
Number separator format | Phone number format |
default | (XXX) XXX-XXXX |
dots | XXX.XXX.XXXX |
hyphens | XXX-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:
format
parameter.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.
Open website/src/app/contacts/shared/phone-number, and you should see the files described in table 5.2.
File | Description |
index.ts | You 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
instead of the more verbose
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.ts | This file contains the country dialing codes that your PhoneNumber model uses. |
phone-number-error-messages.ts | This file contains all the error messages that PhoneNumberPipe and the PhoneNumber model use. |
phone-number.model.ts | This 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.ts | This 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.
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.
Test case | Number | Displays |
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 |
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.
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.
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.
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.
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.
format
parameterTest case | Format | Number | Displays |
If 'default' is used or no parameter is specified, then the number will be in the default (XXX) XXX-XXXX format. | default | 7035550123 | (703) 555-0123 |
If 'dots' is passed in as a parameter, then the number should be in XXX.XXX.XXXX format. | dots | 7035550123 | 703.555.0123 |
If 'hyphens' is passed in as a parameter, then the number should be in XXX-XXX-XXXX format. | hyphens | 7035550123 | 703-555-0123 |
If an unrecognized format is passed in as a parameter, then the default (XXX) XXX-XXXX format should be used. | gibberish | 7035550123 | (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.
Now that you understand how to test the first parameter, it's time for a little 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.
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.
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.
Test case | Number | Country code | Displays |
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. | 7035550123 | us | + 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. | 7035550123 | zz | 703.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.
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:
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.
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.3.146.152.99