Skip to content

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'; -- use DebugElement to inspect an element during testing, similar to the native HTMLElement 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 debugging
    • TestBed --- 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 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 , a tick 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'; --- use NoopAnimations class to mock animations, which allows tests to run quickly without waiting for the animations to
  • import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; --- helps bootstrap the browser to be used for testing
  • import { 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 the ComponentFixture, which contains methods that help debug and test a component
  • component --- Stores an instance of the ContactEditComponent
  • 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.