Angular applications are built from components, so the most important place to start when testing an Angular application is with component tests. For example, imagine a component that displays a calendar. It might enable a user to select a date, change the selected date, cycle through months and years, and so on. You need to write a test case for each of these pieces of functionality.
In this chapter, we’ll cover key testing classes and functions, such as TestBed, ComponentFixture,
and fakeAsync
, which help you test your components. You’ll need a good grasp of these classes and functions to write component tests.
Don’t worry if you've never heard of these concepts before. You’ll practice using them as we go. By the end of this chapter, you’ll understand what components are and know how to write tests for them. Let’s kick off the chapter by looking at some basic component tests.
The best way to get comfortable writing component tests is to write a few tests for a basic component. In this section, you’ll write tests for the ContactsComponent
component. ContactsComponent
has almost no functionality and will be easy to test.
To get started, follow the instructions for setting up the example project in appendix A, if you haven't already. Then, navigate into the testing-angular-applications directory, create a file named contacts.component.spec.ts in the website/src/app/contacts/ directory, and open it in your text editor or IDE. (While you’re there, if you want to take a peek at the source code for ContactsComponent
that you’ll be writing tests against, open the contacts.component.ts file.)
The first step in creating your test is to import your dependencies. This kind of test requires two dependencies. The first is ContactsComponent
in the contacts.component
module. At the top of the file, add the following line:
import { ContactsComponent } from './contacts.component';
The second dependency you need to import is the interface that defines a contact. Immediately after the first import statement, add the following line of code:
import { Contact } from './shared/models';
Now, you’ll create the test suite that will house all your tests for ContactsComponent
. After the import
statement, add a describe
block to create your test suite:
describe('ContactsComponent Tests', () => {
});
Next, you need to create a variable named contactsComponent
that references an instance of ContactsComponent
. You’ll set the contactsComponent
variable in the beforeEach
block of your tests. Doing so will guarantee that you’re generating a new instance of ContactsComponent
when each test runs, which will prevent your test cases from interfering with each other. On the first line inside the describe
callback function, add the following code:
Add a beforeEach
function that sets the contactsComponent
variable to a new instance of ContactsComponent
before each test is executed:
beforeEach(() => {
contactsComponent = new ContactsComponent();
});
Your first test validates that you can create an instance of ContactsComponent
properly. Add the following code below the beforeEach
statement:
it('should set instance correctly', () -> {
expect(contactsComponent).not.toBeNull();
});
After adding this code snippet, your contacts.component.spec.ts file should look like the code in the following listing.
Listing 3.1 contacts.component.spec.ts
import { ContactsComponent } from './contacts.component';
import { Contact } from './shared/models';
describe('ContactsComponent Tests', () => {
let contactsComponent: ContactsComponent = null; ①
beforeEach(() => {
contactsComponent = new ContactsComponent(); ②
});
it('should set instance correctly', () => {
expect(contactsComponent).notBeNull(); ③
});
});
This is a simple test, and if the contactsComponent
variable contains anything other than null, the test will pass.
Run ng test
to run your first test. You’ll see one passing test in the Chrome window (figure 3.1). If you get an error, examine the error messages to see where the test is failing.
You need to write a few more basic tests to finish the tests for ContactsComponent
. For the next test, let’s see what happens if the component contains no contacts. Without any contacts, the contacts
array length should be zero, because the contacts
array is empty by default. Add the following test to your test file:
it('should be no contacts if there is no data', () => {
expect(contactsComponent.contacts.length).toBe(0);
});
For the last test, you’ll make sure that you can add contacts to the list. To do this, create a new contact using the Contact
interface and add it to an array called contactsList
. Finally, set the contacts
property of ContactsComponent
to the contactsList
array that you created. To do this, add the following code after the previous test:
it('should be contacts if there is data', () => {
const newContact: Contact = {
id: 1,
name: 'Jason Pipemaker'
};
const contactsList: Array<Contact> = [newContact];
contactsComponent.contacts = contactsList;
expect(contactsComponent.contacts.length).toBe(1);
});
Your completed contacts.component.spec.ts test should look like the following listing.
Listing 3.2 Completed contacts.component.spec.ts file
import { ContactsComponent } from './contacts.component';
import { Contact } from './shared/models';
describe('ContactsComponent Tests', () => {
let contactsComponent: ContactsComponent = null;
beforeEach(() => {
contactsComponent = new ContactsComponent();
});
it('should set instance correctly', () => {
expect(contactsComponent).not.toBeNull();
});
it('should be no contacts if there is no data', () => {
expect(contactsComponent.contacts.length).toBe(0); ①
});
it('should be contacts if there is data', () => {
const newContact: Contact = {
id: 1,
name: 'Jason Pipemaker'
};
const contactsList: Array<Contact> = [newContact];
contactsComponent.contacts = contactsList;
expect(contactsComponent.contacts.length).toBe(1); ②
});
});
If your test process is still running, you should see three passing unit tests in the Chrome test-runner window (figure 3.2).
If you see any errors, check your code against the GitHub repository at http://mng.bz/1BFL.
So far, your tests haven’t needed any Angular-specific dependencies because ContactsComponent
is a normal TypeScript class. When testing these types of components, you don’t need any help from the Angular testing modules. These types of tests are known as isolated tests because they don’t need any Angular dependencies and you can treat them like ordinary TypeScript files.
You might need to write this kind of test when a component has limited functionality. For example, let’s say you’ve created a component for a new page, like a sign-up page, but you haven’t implemented the logic yet. You could write a couple of isolated tests to make sure the component is created correctly.
You’ve warmed up your component testing skills a bit, so in the next section you can write tests for a component with more functionality.
In the real world, you’ll need to test more complex components. For example, say you want to test a sidebar that contains a menu. You’d like to be able to test the sidebar without worrying about the menu. In such situations, you can use what are known as shallow tests. Shallow tests let you test components one level deep, ignoring any child elements that the element may contain; you can test the parent component in isolation.
In this section, you’ll write shallow tests for the ContactEditComponent
component.
ContactEditComponent
is similar to components used in real applications, so it’s a good example to write tests against. Navigate to website/src/app/contacts/contact-edit in the project directory and create a file named contact-edit.component.spec.ts. You’ll start by importing the necessary dependencies.
Because ContactEditComponent
is a fully functioning component, it requires a lot of dependencies. Your tests will reflect that in the number of import statements that you need. Let’s consider the imports in the following order:
These dependencies are required for your test module to work. Let’s see how to meet that requirement by diving into the Angular dependencies.
The following list provides a walkthrough of the import statements you’ll need for your tests:
import { DebugElement } from '@angular/core';
—You can use DebugElement
to inspect an element during testing. You can think of it as the native HTMLElement
with additional methods and properties that can be useful for debugging elements.import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
ComponentFixture
—You can find this class in the @angular/core
module. You can use it to create a fixture that you then can use for debugging. TestBed
—You use this class to set up and configure your tests. Because you use TestBed
anytime you want to write a unit test for components, directives, and services, it’s one of the most important utilities that Angular provides for testing. In this book, you’ll be using the configureTestingModule, overrideModule,
and createComponent
methods, which you’ll put to use later in the chapter. Because the API for TestBed
is extensive,
we only scratch the surface of the API in this book. If you want to see what else belongs to the TestBed
API, we recommend visiting https://angular.io/api/core/testing/TestBed.fakeAsync
—Using fakeAsync
ensure that all asynchronous tasks are completed before executing the assertions. Not using fakeAsync
may cause the test to fail because the assertions may be executed without all of the asynchronous tasks not being completed. When using fakeAsync
, you can use tick
to simulate the passage of time. It accepts one parameter, which is the number of milliseconds to move time forward. If you don’t provide a parameter, tick
defaults to zero milliseconds. import { By } from '@angular/platform-browser';
—By
is a class included in the @angular/platform-browser
module that you can use to select DOM elements. For example, let’s say you want to select an element with the CSS class name of highlight-row;.
The element may look like the following HTML element:<i class="highlight-row">
You would use
the css
method to retrieve that element using a CSS selector. The resulting code would look like this:
By.css('.highlight-row')
Note that you use a period to select the elements by CSS class name. In total, By
provides three methods, which you can find in table 3.1.
By
methodsMethod | Description | Parameter |
all | Using all will return all of the elements. | None |
css | Using a CSS attribute, you can select certain elements. | CSS attribute |
directive | You can use the name of a directive to select elements. | Directive name |
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
—You use the NoopAnimationsModule
class
to mock animations, which allows tests to run quickly without waiting for the animations to finish.import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
—BrowserDynamicTestingModule
is a module that helps bootstrap the browser to be used for testing.
import { RouterTestingModule } from '@angular/router/testing';
—As the name implies, you can use RouterTestingModule
to set up routing for testing. We include it with the tests for this component because some of the actions will involve changing routes.At the top of your contact-edit.component.spec.ts file, add the import statements from Angular shown in the following listing.
Listing 3.3 Completed contact-edit.component.spec.ts file
import { DebugElement } from '@angular/core'; ①
import { ComponentFixture, fakeAsync, TestBed, tick } from
'@angular/core/testing'; ②
import { By } from '@angular/platform-browser'; ③
import { NoopAnimationsModule } from
'@angular/platform-browser/animations'; ④
import { BrowserDynamicTestingModule } from ⑤
'@angular/platform-browser-dynamic/testing'; ⑥
import { RouterTestingModule } from '@angular/router/testing'; ⑤
We covered quite a bit in these import
statements. You’ll be using all of these statements and will find all of them useful, but pay special attention to the most important classes and functions: TestBed
, ComponentFixture
, and fakeAsync
.
You only need to import one Angular nontesting module—FormsModule
. You need this module because the ContactEditComponent
uses it for some Angular form controls. Right after the import
statements that you added, add the following import
statement:
import { FormsModule } from '@angular/forms';
Now that we’ve covered the major classes and methods included in the Angular framework that you’ll use, you can add the rest of the dependencies you’ll need to finish the tests. Add the following lines of code after the existing imports:
import { Contact, ContactService, FavoriteIconDirective, InvalidEmailModalComponent, InvalidPhoneNumberModalComponent } from
'../shared';
import { AppMaterialModule } from '../app.material.module';
import { ContactEditComponent } from './contact-edit.component';
import '../../../material-app-theme.scss';
Verify that your imports section looks like the code in the following listing before continuing.
Listing 3.4 contact-edit.component.spec.ts imports section
import { DebugElement } from '@angular/core';
①
import { ComponentFixture, fakeAsync, TestBed, tick } from
'@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from
'@angular/platform-browser/animations';
import { BrowserDynamicTestingModule } from
'@angular/platform-browser-dynamic/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule } from '@angular/forms';
②
import { Contact, ContactService, FavoriteIconDirective,
InvalidEmailModalComponent, InvalidPhoneNumberModalComponent } from
'../shared';
③
import { AppMaterialModule } from '../app.material.module';
import { ContactEditComponent } from './contact-edit.component';
Next, you’ll set up the tests.
The first step in setting up your tests is to create the describe
block that will house all your tests and declare the instance variables they need. Beneath the import
statements, add the following code:
describe('ContactEditComponent tests', () => {
let fixture: ComponentFixture<ContactEditComponent>;
let component: ContactEditComponent;
let rootElement: DebugElement;
});
The describe
method creates the test suite that contains all your tests. As for the instance variables:
fixture
—Stores an instance of the ComponentFixture
, which contains methods that help you debug and test a componentcomponent
—Stores an instance of the ContactEditComponent
rootElement
—Stores the DebugElement
for your component, which is how you’ll access its childrenYou’ll use a test fake for ContactService
because the real ContactService
makes HTTP calls, which would make your tests harder to run and less deterministic. Also, faking ContactService
allows you to focus on testing the ContactEditComponent
without worrying about how ContactService
works. Angular’s dependency injection system makes it easy to instantiate a ContactEditComponent
with a fake version of ContactService
. The fake ContactService
has the same type as the real one, so the TypeScript compiler will throw an error if you forget to stub out part of the interface.
Right after the last variable declaration, but still inside the describe
block, add the code in the following listing to create the fake service named contactServiceStub
.
Listing 3.5 Mock ContactService
const contactServiceStub = {
contact: { ①
id: 1,
name: 'janet'
},
save: async function (contact: Contact) { ②
component.contact = contact;
},
getContact: async function () { ③
component.contact = this.contact;
return this.contact;
},
updateContact: async function (contact: Contact) { ④
component.contact = contact;
}
};
Now that you have a fake ContactService
, add two beforeEach
blocks, which will execute before each test. The first beforeEach
sets up your TestBed
configuration. The second will set your instance variables. You could have just one beforeEach
, but your test will be easier to read if you keep them separate.
A lot needs to happen now, as you can see in listing 3.6, so let’s break down the code a bit. The TestBed
class has a method called configureTestingModule
.
You can probably guess its purpose, which is to configure the testing module. It’s much like the NgModule
class that’s included in the app.module.ts file, which you can find at src/app. The only difference is that you only use configureTestingModule
in tests. It
takes an object that’s in the format of a TestModuleMetadata
type alias. If you aren’t familiar with a type alias, for our purposes you can think of it like an interface. In the code listing, note the providers section:
providers: [{provide: ContactService, useValue: contactServiceStub}]
This is where you provide your fake contact service contactServiceStub
in place of the real ContactService
with useValue
.
You use overrideModule
in this case because you need the two modal dialogs to be loaded lazily. Lazy loading means that the dialogs won’t be loaded until the user performs an action to cause them to load. Currently, the only way to do this is to use overrideModule
and set
the entryComponents
value to an array that contains the two modal components that the ContactEditComponent
uses—InvalidEmailModalComponent
and InvalidPhoneNumberModalComponent
.
Finally,
the last line of this first beforeEach
statement
uses
TestBed
.
get
(
ContactService
)
to
get a reference to your fake
contactService
from Angular’s dependency injector. This will be the same instance that ContactEditComponent
uses.
After the code for contactServiceStub
, add the code in the following listing as your first beforeEach
statement.
Listing 3.6 First beforeEach
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ContactEditComponent, FavoriteIconDirective,
InvalidEmailModalComponent, InvalidPhoneNumberModalComponent],
imports: [
AppMaterialModule,
FormsModule,
NoopAnimationsModule,
RouterTestingModule
],
providers: [{provide: ContactService,
useValue: contactServiceStub}] ①
}); ②
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [InvalidEmailModalComponent,
InvalidPhoneNumberModalComponent]
}
}); ③
});
You can see in the listing that TestModuleMetadata
accepts four optional properties, which are described in table 3.2.
TestModuleMetadata
optional fieldsField | Data Type | Description |
declarations | any[ ] | This is where you list any components that the component you’re testing may need. |
imports | any[ ] | You set imports to an array of modules that the component you’re testing requires. |
providers | any[ ] | Lets you override the providers Angular uses for dependency injection. In this case, you inject a fake ContactService. |
schemas | Array<SchemaMetadata | any[ ]> | You can use schemas like CUSTOM_ELEMENTS_SCHEMA and NO_ERRORS_SCHEMA to allow for certain properties of elements. For example, the NO_ERRORS_SCHEMA will allow for any element that’s going to be tested to have any property. |
Now you’ll add the second beforeEach
statement. The fixture
variable stores the component-like object from the TestBed.createComponent
method that you can use for debugging and testing, which we mentioned earlier. The component
variable holds a component that you get from your
fixture
using the
componentInstance
property.
But what is this
fixture.detectChanges
method that you haven’t seen before? The detectChanges
method triggers a change-detection cycle for the component; you need to call it after initializing a component or changing a data-bound property value. After calling detectChanges
, the updates to your component will be rendered in the DOM. In production, Angular uses something called zones (which you’ll learn more about in Chapter 9) to know when to run change detection, but in unit tests, you don’t have that mechanism. Instead, you need to call detectChanges
frequently in your tests after making changes to a component.
Directly after the first beforeEach
statement, add in the following code:
beforeEach(() => {
fixture = TestBed.createComponent(ContactEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
rootElement = fixture.debugElement;
});
So far, so good. You’ve added the code to set up your tests. In the next section, you’ll add the tests themselves.
You’re ready to write your tests. You want to test the saveContact
, loadContact
, and updateContact
methods for ContactEditComponent
because those methods hold most of the functionality of the component. The ContactEditComponent
class has several more private helper methods, but you don’t need those because testing the component’s public API will exercise them. In general, you shouldn’t test private methods; if a method is important enough to be tested, you should consider making it public.
First, you should write a test for the saveContact
method. Calling saveContact
changes the component’s state, which will be reflected in changes to the DOM. You’ll use the fakeAsync
method to keep the test from finishing until the component has finished updating.
Next, create a contact object and set the component.isLoading
property to false
. You need to do this manually; otherwise, all that will render is the loading-progress bar. Then you’ll call the saveContact
method to save the contact that’s stored in the contact
variable. Normally, saveContact
would use the real ContactService
, but because you configured the testing module to provide contactServiceStub
earlier, the component will call the stub.
After you’ve called the saveContact
method, you’ll notice that you call detectChanges
. As mentioned earlier, after you make changes to components, you need to call detectChanges
so that those changes will be rendered, which allows you to test that changes to the component are reflected in the DOM.
After calling detectChanges
, query rootElement
using By.css
for the contact-name
class to get the input element that contains the contact name. Then call tick
to simulate the passage of time so the component will finish updating. Notice that the tick
method doesn’t have a parameter for milliseconds, so it uses the default value of zero milliseconds. Finally, assert that the value of nameInput
is equal to lorace
.
Add the code in the following listing directly after the last beforeEach
statement. Make sure you stay within the overall test suite (the top-level describe
block).
Listing 3.7 saveContact
method test
describe('saveContact() test', () => {
it('should display contact name after contact set', fakeAsync(() => {
const contact = { ①
id: 1,
name: 'lorace'
};
component.isLoading = false; ②
component.saveContact(contact); ③
fixture.detectChanges(); ④
const nameInput = rootElement.query(By.css('.contact-name')); ⑤
tick(); ⑥
expect(nameInput.nativeElement.value).toBe('lorace'); ⑦
}));
});
Next, you’ll write a test for the loadContact
method. This test is similar to the test in listing 3.7. The only difference is that you’ll use the loadContact
method instead of the saveContact
method of the ContactEditComponent
class. The loadContact
method will load a contact for your testing purposes inside the contactServiceStub
.
The contact’s name is janet,
which is the value you’ll use in the assertion.
Add the code from the following listing directly after the saveContact
method test that you just created.
Listing 3.8 loadContact
method test
describe('loadContact() test', () => {
it('should load contact', fakeAsync(() => {
component.isLoading = false;
component.loadContact(); ①
fixture.detectChanges();
const nameInput = rootElement.query(By.css('.contact-name'));
tick();
expect(nameInput.nativeElement.value).toBe('janet'); ②
}));
});
Now we’ll move on to testing the updateContact
method.
By now, you’ve probably picked up on a pattern: this test is similar to the other two tests. This time, you first set a contact that has a name of rhonda
and test that the component renders correctly. The major difference between this test and the other two tests is that it uses a second assertion. You want to check to see that the name updates when you call updateContact
. To do this, call updateContact
and pass it newContact
.
You might notice that you call tick
in the following listing with 100 as a parameter. You need this time because the updateContact
method takes a bit longer to execute than the other methods that you’ve been testing. Add the code from the following listing after the previous test.
Listing 3.9 First updateContact
method test
describe('updateContact() tests', () => {
it('should update the contact', fakeAsync(() => {
const newContact = {
id: 1,
name: 'delia',
email: '[email protected]',
number: '1234567890'
};
component.contact = {
id: 2,
name: 'rhonda',
email: '[email protected]',
number: '1234567890'
};
component.isLoading = false;
fixture.detectChanges();
const nameInput = rootElement.query(By.css('.contact-name'));
tick();
expect(nameInput.nativeElement.value).toBe('rhonda');
component.updateContact(newContact); ①
fixture.detectChanges(); ②
tick(100); ③
expect(nameInput.nativeElement.value).toBe('delia'); ④
}));
});
Run ng t
in your console (if you haven’t already). You should see six passing tests. If you don’t see six passing tests, go back to the code samples and make sure your code matches the code in this book.
You now have a test that will update a contact, but you need to test what happens when you try to update the contact with invalid contact data. First, see what happens when you try to update the contact with an invalid email address. The differences between listing 3.9 and 3.10 are highlighted in bold. The newContact
variable now has an invalid email, and the last assertion doesn’t expect the contact to change because the email is invalid. That’s why both assertions expect the contact’s name to remain chauncey
. Add the code in the following listing directly after your first updateContact
method test.
Listing 3.10 Second updateContact
method test
it('should not update the contact if email is invalid', fakeAsync(() => {
const newContact = {
id: 1,
name: 'london',
email: 'london@example', ①
number: '1234567890'
};
component.contact = {
id: 2,
name: 'chauncey',
email: '[email protected]',
number: '1234567890'
};
component.isLoading = false;
fixture.detectChanges();
const nameInput = rootElement.query(By.css('.contact-name'));
tick();
expect(nameInput.nativeElement.value).toBe('chauncey');
component.updateContact(newContact);
fixture.detectChanges();
tick(100);
expect(nameInput.nativeElement.value).toBe('chauncey'); ②
}));
Now let’s see what happens when you try to update a contact with an invalid phone number. Again, notice the bolded code in listing 3.11. The only difference between this test and the previous test is that the number now contains too many digits. Similar to the test before this one, the contact name is the same in both assertions.
Add the code in the following listing to the end of the second updateContact
method test that you just wrote.
Listing 3.11 Third updateContact
method test
it('should not update the contact if phone number is invalid',
fakeAsync(() => {
const newContact = {
id: 1,
name: 'london',
email: '[email protected]',
number: '12345678901' ①
};
component.contact = {
id: 2,
name: 'chauncey',
email: '[email protected]',
number: '1234567890'
};
component.isLoading = false;
fixture.detectChanges();
const nameInput = rootElement.query(By.css('.contact-name'));
tick();
expect(nameInput.nativeElement.value).toBe('chauncey');
component.updateContact(newContact);
fixture.detectChanges();
tick(100);
expect(nameInput.nativeElement.value).toBe('chauncey'); ②
}));
Run ng t
in your terminal again. You should see eight passing tests. If you see any errors, try checking your code against the version in the GitHub repository at http://mng.bz/Ud5b.
You’ve coded complete test coverage for a real-world component! You’re likely to come across components out there that are more advanced, but what you’ve learned here gives you a foundation for writing tests that can handle that complexity. Components are one of the most—if not the most—important concepts in Angular, so you need a firm understanding of the component testing basics to be successful in writing tests.
fakeAsync
function, you can ensure that all asynchronous calls are completed within a test before the assertions are executed. Doing so prevents test from failing unexpectedly before all of the asynchronous calls are completed.ComponentFixture
class to debug an element.TestBed
is a class that you use to set up and configure your tests. Use it anytime you want to write a unit test that tests components, directives, and services.DebugElement
to dive deeper into an element. You can think of it as the HTMLElement
, with methods and properties added that can be useful for debugging elements.nativeElement
object is an Angular wrapper around the built-in DOM native element.18.116.43.127