Angular/ Angular Unit Testing
Introduction
Most Angular testing involves two types of test: Unit tests and E2E tests. The following examples follow the Testing Angular Applications book published by Manning Publications. The testing examples in the book are available in the following repository:
Code examples are provided to help acquire hands on experience.
The completed code for this unit testing demonstration,
along with all testing examples can be found in the
owf-learn-angular-test repository.
This specific demonstration is titled simple-unit-testing-app
within that repository.
Why Unit Testing?
Unit tests are used to test the functionality of units of code. Unit tests should only test one aspect of the source code. This can include testing functions, methods, objects, types, values, and more. Unit tests should be fast, reliable, and repeatable so that they can be run efficiently to confirm software functionality.
Getting Started
Initial Test file
Before beginning the following demonstration, the initial test file that is created whenever a new Angular project is created will be reviewed.
When a new Angular project is created, a default component and test file are added. A test script for the test file is also always created alongside any component module that is created using Angular CLI.
Test file names always end with .spec.ts
.
A test script named app.component.spec.ts
is automatically generated when a new Angular project is created.
The following is an example of an initial test script file generated after creating a new Angular project named empty-angular-app
.
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'empty-angular-app'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('empty-angular-app');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to test-empty!');
});
});
This is the standard layout for auto-generated test scripts for a new Angular project. Tests will most likely fail once functionality is added. The tests will need to be updated to be consistent with current code and run as expected.
Unit Testing Sample Project
To learn about unit testing, start with the sample project used in this documentation. Navigate to a folder and clone the repository using the following command:
git clone https://github.com/testing-angular-applications/testing-angular-applications.git
Then navigate to folder with the Angular applications files:
cd testing-angular-applications/website
Make sure that all the necessary software dependencies for the application are installed by running following command:
npm install
Finally, to run the application, run the following on the command line, in the website
directory:
ng serve --open
Note:
By default, when a new project is created using ng new appName
,
a component and test file for that component are automatically generated.
Test files will automatically be generated when created components,
services and other files when using the ng generate <schematic>
command.
Basic Unit Tests
Writing tests using Jasmine
Jasmine is a behavior-driven development (BDD) framework that is popular among testing JavaScript applications. The advantage of writing tests using BDD is that the test code will read similar to plain English. Although Jasmine tests can be written in JavaScript, all tests in this guide are written in TypeScript.
Below are essential Jasmine functions to know.
describe
describe(string describing the test suit, callback);
In Jasmine, the describe
function is used to group tests together.
This is known as a test suite. The describe
function take two parameters,
a string and a callback function as shown above.
The number of describe functions depends on how test suites are organized.
it
it(string describing the test, callback);
The it
function's purpose is to test a behavior of code.
it
refers to the object/component/class/whatever that is being tested rather than a method,
and should read like a sentence.
Example:
it('should do this in your test.')
It is common syntax to start test cases with should do x
.
If this is done consistently, the code reads like a sentence and makes it easier to read.
expect
The expect
function is present in the code that confirms whether or not the test works.
This section of the code is known as the assertion because the test is asserting something as being true.
There are two parts in the assertion: the expect
function and the matcher.
The expect
function is where the assertion check is defined
and is chained together with a matcher such as toBe(true)
.
Your First Jasmine Test
To begin writing your first Jasmine test first navigate to the website/src/app
folder
and create a file named first-jasmine-test.spec.ts
.
The typical naming convention for the test file is to be consistent with format: <name of file>.spec.ts
.
In the first-jasmine-test.spec.ts
file, add the code shown below:
describe('Chapter 2 tests', () => { // Groups tests into a test suite
it('Out first Jasmin test', () => { // Separates individual tests
expect(true).toBe(true); // Assertion (true) & matcher toBe()
});
});
Run the Unit test
Use the Angular CLI command to execute the unit tests.
ng test
If ng test
is run, any file that ends with .spec.ts
will run.
A browser should open automatically.
The output should indicate that the first test has passed.
Next write a test that proves 2+2 equals 4.
Modify the previous describe
function to have additional it
, as follows:
describe('Chapter 2 tests', () => {
it('Out first Jasmin test', () => {
expect(true).toBe(true);
});
it('2 + 2 equals 4', () => { // Second test
expect(2 + 2).toBe(4);
});
});
The test results should indicate that there are 2 specs and 0 failures.
To see what a failure case looks like, change the toBe()
part of the assertion to something other than 4
.
This will result in 2 specs and 1 failure.
If desired the initial trivial test file first-jasmine-test.spec.ts
can be removed since it is trivial
rm first-jasmine-test.spec.ts
Unit Tests for Classes
This demonstration will define a test for a class named ContactClass
.
ContactClass
holds a person's contact information including the person's ID,
name, email, phone number, country, and whether or not they are a favorite.
To begin, navigate to the website/src/app/contacts/shared/models/
folder.
The ContactClass
is in the folder in the contact.ts
file.
In this same folder create a file named contact.spec.ts
.
Step 1: In contact.spec.ts
, add code to import dependencies.
Since the ContactClass
class from the contact
module is being tested,
add the following code at the top of the file.
import ContactClass from './contact';
Note: Although the module's file name is contact.ts
, the .ts
file extension does not need to be included because it is optional in import
statements.
Step 2: Create a test suite using the describe
method and name the test suite Contact Class Tests
.
decribe ('contact class Tests', () => {
});
Step 3: Within the describe
function,
create a variable that hold the instance of the Contactclass
and set it to null
.
Add this code inside of the describe
test suite:
let contact: ContactClass = null;
Step 4: Initialize the contact
variable.
This part of the test is known as the Setup.
In the setup, to initialize the contact
variable every time a test runs,
use a beforeEach
method.
Add the following code directly beneath the contact variable declaration:
beforeEach( () => {
contact = new ContactClass();
});
A variable is typically every time a test is run because it ensures that each test runs independently and that previously manipulated variables do not interfere with any subsequent tests.
Step 5: Write a test for the default constructor.
The test will confirm that that an instance of ContactClass
is successfully
created using the class' constructor.
Test this by seeing if the contact
is not null
.
Add this code directly bellow the beforeEach
method:
it('should have a valid contructor', () => {
expect(contact).not.toBeNull();
});
Because the ContactClass
class does have a valid constructor,
the not.toBeNull()
matcher will evaluate to true
and the test will pass.
Step 6: Write a test for the ContactClass
constructor with name as input.
To check whether the name
property is set correctly,
test the get
method for the name
properties.
Add the following new test directly below the constructor test:
it('should set name correctly through constructor', () => {
contact = new ConctactClass('Liz');
expect(contact.name).toEqual('Liz');
});
Step 7: Implement a teardown for the test suite.
Use the teardown part of the test to make sure instances of variables get destroyed.
This will help avoid memory leaks. To do this, add an afterEach
function and set the contact
variable to null
.
Insert the following code directly after the test:
afterEach(() => {
contact = null;
});
The contact.spec.ts
file should look like the following:
import ContactClass from './contact';
describe('Contact class tests', () => {
let contact: ContactClass = null
beforeEach( () => {
contact = new ContactClass();
});
it('should have a valid constructor', () => {
expect(contact).not.toBeNull();
});
it('should set name correctly through constructor', () => {
contact = new ConctactClass('Liz');
expect(contact.name).toEqual('Liz');
});
afterEach( () => {
contact = null;
});
});
Code with comments:
import ContactClass from './contact';
describe('Contact class tests', () => {
// create a variable to hold your instance of the ContactClass and set to null
let contact: ContactClass = null
// *setup*
// initialize contact variable every time test runs
beforeEach( () => {
contact = new ContactClass();
});
it('should have a valid constructor', () => {
expect(contact).not.toBeNull();
});
// tests for setting name using a constructor and testing the getter for name
it('should set name correctly through constructor', () => {
contact = new ContactClass('Liz');
expect(contact.name).toEqual('Liz');
});
// tear down
// make sure instances of variables get destroyed to avoid memory leaks
afterEach( () => {
contact = null;
});
});
Run the new test by running the following command within the website directory:
ng test
Additional Testing
Next add tests to adequately test the ContactClass
class.
To test the getters and setters for the id
and name
properties of the class,
add the following two tests:
Getters and Setters for the ID property:
it('should get and set id correctly', () => {
contact.id = 1;
expect(contact.id).toEqual(1);
});
Getters and Setters for the name property:
it('should get and set name correctly', () => {
contact.name = 'Liz';
expect(contact.name).toEqual('Liz');
});
Exercise
To aim for 100% test coverage for any given module, tests should exercise every line in the module.
To do this for the ContactClass
class,
write additional tests to those above.
The complete test code is as follows.
import ContactClass from './contact';
describe('Contact class tests', () => {
let contact: ContactClass = null
beforeEach( () => {
contact = new ContactClass();
});
it('should have a valid constructor', () => {
expect(contact).not.toBeNull();
});
it('should set name correctly through constructor', () => {
contact = new ContactClass('Liz');
expect(contact.name).toEqual('Liz');
});
it('should get and set id correctly', () => {
contact.id = 1;
expect(contact.id).toEqual(1);
});
it('should get and set name correctly', () => {
contact.name = 'Liz';
expect(contact.name).toEqual('Liz');
});
it('should get and set email correctly', () => {
contact.email = 'Liz@gmail.com';
expect(contact.email).toEqual('Liz@gmail.com');
});
it('should get and set number correctly', () => {
contact.number = '555 5555 5555';
expect(contact.number).toEqual('555 5555 5555');
});
it('shoulg get and set country correctly', () => {
contact.country = 'Mexico';
expect(contact.country).toEqual('Mexico');
});
it('should get and set favorite correctly', () => {
contact.favorite = true;
expect(contact.favorite).toEqual(true);
});
afterEach( () => {
contact = null;
});
});
Testing Components
The following demonstration defines tests for the ContactsComponent
component.
This component has little functionality and will consequently be easy to test.
First navigate to the website/src/app/contacts
folder and create a file named contacts.component.spec.ts
.
Step 1: Import dependencies.
This test requires two dependencies.
The first dependency is ContactsComponent
in the contacts.component
module.
At the top of the file insert the following code:
import { ContactsComponent } from './contacts.component';
The next dependency to import is the interface that defines a contact. Insert the following code immediately after the first import statement:
import { Contact } from '.shared/models';
Step 2: Create test suite.
Create the test suite that will house all of your tests for ContactsComponent
.
Add a describe
block to create the test suite:
describe('ContactsComponent Tests', () => {
});
Step 3: Create the test variable for instance.
Create a variable named contactsComponent
that references an instance of ContactsComponent
.
Add the following code in the first line of the describe
block:
let contactsComponent: ContactsComponent = null;
Step 4: Initialize the test variable.
Set the contactsComponent
variable in the beforeEach
block of the tests.
This is to guarantee that a new instance of ContactsComponent
is generated when each test runs,
thus preventing the test from interfering with each other tests.
Add a beforeEach
function that sets the contactsComponent
variable to a new instance of ContactsComponent
before each test is executed:
beforeEach(() => {
contactsComponent = new ContactsCompenent();
});
Step 5: Add tests
Test 1:
The first test will validate that an instance of ContactsComponent
can be properly created.
Add the following test within the beforeEach
statement:
it('should set instance correctly', () => {
expect(contactsComponent).not.toBeNull();
});
After adding this code, the contacts.component.spec.ts
should look like this:
import { ContactsComponent } from "./contacts.component";
import { Contact } from "./shared/models";
describe('ContactsComponent Tests', () => {
let tcontactsComponent: ContactsComponent = null;
beforeEach( () => {
tcontactsComponent = new ContactsComponent
});
it('should set instance correctly', () => {
expect(tcontactsComponent).not.toBeNull();
});
});
For this test, if the contactsComponent
variable contains anything other than null, the test will pass.
Define a few more tests to finish the tests for ContactsComponent
.
Test 2:
Test what would happen if the component contains no contacts.
If there are no contacts, then the contacts
array should be zero. Add the following test to the file.
it('should have no contacts if there is no data', () => {
expect(contactsComponent.contacts.length).toBe(0);
});
Test 3:
The last test will make sure that contacts can be added to the list.
To do this, create a new contact using the Contact
interface and add it to an array called contactList
.
Then set the contacts
property of ContactsComponent
to the contactList
array.
Add the following code after the previous test:
it('should have contacs if there is data', () => {
const newContact: Contact = {
id: 1,
name: 'Jason Pipemaker',
};
const contactList:Array<Contact> = [newContact];
contactsComponent.contacts = contactsList;
expect(contactsComponent.contacts.length).toBe(1);
});
The completed code should look like the following:
import { ContactsComponent } from "./contacts.component";
import { Contact } from "./shared/models";
describe('ContactsComponent Tests', () => {
let contactsComponent: ContactsComponent = null;
beforeEach( () => {
contactsComponent = new ContactsComponent
});
// test whether the component is set correctly
it('should set instance correctly', () => {
expect(contactsComponent).not.toBeNull();
});
// test that there should be no contacts by default
it('should have no contacts if there is no data', () => {
expect(contactsComponent.contacts.length).toBe(0);
});
// test if one contact is added then the number of contacts in the array should be 1
it('should have contacts if there is data', () => {
// create a new contact of type Contact
const newContact: Contact = {
id: 1,
name: 'Jason Pipemaker'
};
// create a contact list that is an array of contacts
// fill that array (contactList) with the new contact
const contactList: Array<Contact> = [newContact];
contactsComponent.contacts = contactList;
expect(tcontactsComponent.contacts.length).toBe(1);
});
});
Testing Functional Components
The following demonstration illustrates testing the ContactEditComponent
component.
This component is similar to components used in real applications.
Navigate to the website/src/app.contacts/contact-edit
folder and create a
file named contact-edit.component.spec.ts
.
Step 1: Import dependencies.
Since ContactEditComponent
is a fully functioning component, it has many dependencies:
- Testing dependencies that come from Angular
- Dependencies that are included with Angular
- Dependencies that required for this project
Angular Import Statements
import { DebugElement } from '@angular/core';
-- useDebugElement
to inspect an element during testing, similar to the nativeHTMLElement
with additional methods and properties that can be useful for debugging elements.import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
ComonentFixture
--- used to create a fixture for debuggingTestBed
--- class used to set up and configure tests. It is one of the most important utilities that Angular provides for testing since it is used anytime a unit test is written for components, directives, and services.fakeAsync
---fakeAsync
is used to ensure that all asynchronous tasks are completed before executing the assertions. Not usingfakeAsync
may cause the test to fail because the assertions may be executed without all of the asynchronous tasks not being completed. When usingfakeAsync
, atick
call can be used to simulate the passage of time. It accepts one parameter, which is the number of milliseconds to move time forward. Not providing a parameter will default to zero milliseconds.
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
--- useNoopAnimations
class to mock animations, which allows tests to run quickly without waiting for the animations toimport { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
--- helps bootstrap the browser to be used for testingimport { RouterTestingModule } from '@angular/router/testing';
---RouterTestingModule
is used to set up routing for testing. It is included with the tests for this component because some of the actions will involve changing routes.
Angular NonTesting Module Statement
Only one Angular nontesting module is needed -- FormsModule
.
It is needed because the ContactEditComponent
uses it for some Angular form controls.
import { FormsModule } from '@angular/forms';
Remaining Dependency Statements
The remaining dependencies are specific to the application code. Add the following lines of code after the existing imports:
import { Contact, ContactService, FavoriteIconDirective, InvalidEmailModalComponent, InvalidPhonNumberModalComponent } from '../shared';
import { AppMaterialModule } from, '../../app.material.module';
import { ContactEditComponent } from './contact-edit.component';
import '../../../material-app-theme.scss'
The contact-edit.component.spec.ts
file should look like the following code:
// Angular import statements
import { DebugElement, asNativeElements } 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';
// Angular nontesting module statement
import { FormsModule } from '@angular/forms';
// Dependencies create for this project
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';
Step 2: Setting up tests
Beneath the import
statements, create a test suite using a describe
block that will hold all of the tests and declare the instance variables they need. Insert the following code:
describe('ContactEditComponent Tests', () => {
let fixture: ComponentFixture<ContactEditComponent>;
let component; ContactEditComponent;
let rootElement: DebugElement;
});
Information on the instance variables in the test suite:
fixture
--- Stores an instance of theComponentFixture
, which contains methods that help debug and test a componentcomponent
--- Stores an instance of theContactEditComponent
rootElement
--- Stores for your component, which is how you'll access its children
Step 3:
A "fake" is an object used in a test to substitute for the real object. A mock is a fake that simulates the real object and keeps track of when it's called and what argument it receives. A stub is a simple fake with no logic, and it always returns the same value.
A fake is used for ContactService
because the real ContactService
makes HTTP calls,
which would make the tests harder to run and less deterministic.
Using a fake will also allow testing to focus on the ContactEditComponent
without worrying about how ContactService
works.
Insert the following mock ContactService
following the last variable declaration:
const contactServiceStub = {
// Default contact object
contact: {
id: 1,
name: 'Joe'
},
// Sets the passed-in object to the component's contact property
save: async function(contact: Contact) {
component.contact = contact;
},
// Method that sets the current contact to the
// component's contant property and returna that contact
getContact: async function () {
component.contact = this.contact;
return this.contact;
},
// Method that updates the contact object
updateContact: async function (contact: Contact) {
component.contact = contact;
}
};
Step 4:
Add two beforeEach()
blocks.
The first block will set up the TestBed
configuration and the second will set the instance variables.
The first beforeEach
:
// much like NgModule
// configuing TestBed to be used in tests
beforeEach( () => {
TestBed.configureTestingModule({
declarations: [
ContactEditComponent,
FavoriteIconDirective,
InvalidEmailModalComponent,
InvalidPhoneNumberModalComponent
],
imports: [
AppMaterialModule,
FormsModule,
NoopAnimationsModule,
RouterTestingModule
],
// This is where you use ContactServiveStub instead of the real service
providers: [{
provide: ContactService,
useValue: contactServiceStub
}],
});
// have to use override because a couple of components will be lazily loaded.
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [
InvalidEmailModalComponent,
InvalidPhoneNumberModalComponent
]
}
});
});
Second beforeEach
:
beforeEach( () => {
fixture = TestBed.createComponent(ContactEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
rootElement = fixture.debugElement;
});
Step 5: Adding tests
Testing SaveContact
Method:
describe('saveContact() test', () => {
it('should display contact name after contact set', fakeAsync( () => {
const contact = {
id: 1,
name: 'Will'
};
// Sets isLoading to false to hid the progress bar
component.isLoading = false;
// Saves the contact object
component.saveContact(contact);
// Uses detectChanges method to trigger change detection
fixture.detectChanges();
// gets the nameInput form field
const nameInput = rootElement.query(By.css('.contact-name'));
// Simulates the passage of time using tick
tick();
// checks to see if he name property has been set corectly
expect (nameInput.nativeElement.value).toBe('Will');
}));
});
Testing loadContact
Method:
describe('loadContact() test', () => {
it('should load contact', fakeAsync( () => {
component.isLoading = false;
// Execute the loadContact method
component.loadContact();
fixture.detectChanges();
const nameInput = rootElement.query(By.css('.contact-name'));
tick();
// the default contact that's loaded has a value of 'Joe' for the name // property at the top
expect(nameInput.nativeElement.value).toBe('Joe');
}));
});
Testing updateContact
Method:
describe('updateContact() tests', () => {
it('should update the contact', fakeAsync( () => {
const newContact = {
id: 1,
name: 'Sal',
email: 'Sal@gmail.com',
number: '1234567890'
};
component.contact = {
id: 2,
name: 'Q',
email: 'Q@gmail.com',
number: '6789012345'
};
component.isLoading = false;
fixture.detectChanges();
const nameInput = rootElement.query(By.css('.contact-name'));
tick();
expect(nameInput.nativeElement.value).toBe('Q');
// Update the existing contact to the newContact Object
component.updateContact(newContact);
// triggers change detection
fixture.detectChanges();
// Simulates the passgae of time, in the passage of time in this case 100 ms
tick(100);
// checks to see that the value in the nameInput form field has been changed
expect(nameInput.nativeElement.value).toBe('Sal')
}))
})
Additional tests are also defined to demonstrate what happens when a contact is updated with an invalid phone number and an invalid email address. These two tests have been commented out but are available for to uncomment to observe the testing behavior. The following tests are be found at the end of the file:
//<-------------------- Testing for invalid phone number -------------------------->
// it('should not update the phone number', fakeAsync( () => {
// const newContact = {
// id: 1,
// name: 'Sal',
// email: 'Sal@gmail.com',
// number: '12345678901' // invalid number: too long
// };
// component.contact = {
// id: 2,
// name: 'Q',
// email: 'Q@gmail.com',
// number: '6789012345'
// };
// component.isLoading = false;
// fixture.detectChanges();
// const nameInput = rootElement.query(By.css('.contact-name'));
// tick();
// expect(nameInput.nativeElement.value).toBe('Q');
// // Update the existing contact to the newContact Object
// component.updateContact(newContact);
// // triggers change detection
// fixture.detectChanges();
// // Simulates the passgae of time, in the passage of time in this case 100 miliseconds
// tick(100);
// // checks to see that the value in the nameInput form field has been changed correctly
// expect(nameInput.nativeElement.value).toBe('Q')
// }));
// <--------------------- Testing for invalid email variable------------------->
// it('should not update the email', fakeAsync( () => {
// const newContact = {
// id: 1,
// name: 'Sal',
// email: 'Sal@gmail', // invalid email: missing .com
// number: '12345678901'
// };
// component.contact = {
// id: 2,
// name: 'Q',
// email: 'Q@gmail.com',
// number: '6789012345'
// };
// component.isLoading = false;
// fixture.detectChanges();
// const nameInput = rootElement.query(By.css('.contact-name'));
// tick();
// expect(nameInput.nativeElement.value).toBe('Q');
// // Update the existing contact to the newContact Object
// component.updateContact(newContact);
// // triggers change detection
// fixture.detectChanges();
// // Simulates the passgae of time, in the passage of time in this case 100 miliseconds
// tick(100);
// // checks to see that the value in the nameInput form field has been changed correctly
// expect(nameInput.nativeElement.value).toBe('Q')
// }));
// });
});
});
Final Code
The final testing code should be similar to the code found in the OpenWaterFoundation/owf-learn-angular-test repository under the simple-unit-testing-app angular project.