Angular 2+ Isolated Tests – the easy way to unit test Angular components and services

In this post I’ll explain a quick and easy way to test your angular components and services.

The complete source code for this article is available on github: https://github.com/ConsultDarryl/angular-simple-test-example

So lets dive in.

The first thing you need to do is create a new project and install the node modules. Let’s do this with the angular-cli.

ng new angular-simple-test-example
npm install

Once npm has finished doing its thing, check everything is working:

ng test

Creating something to test

You are going to test the app.component class. Open the file (\src\app\app.component.ts) and copy the following code into it.

import { Component } from '@angular/core';

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})
export class AppComponent {
 
 title = 'app works!';
 counter: number = 1;

constructor() {
 }

plusOne(): void {
 this.counter++;
 }
}

Check the console to make sure everything is still working (ng test runs in the background, watching for changes, until you tell it to stop).

You are going to test the plusone() method.

plusOne() is a very simple method. All it does is increment a counter by 1.

Write a simple test

To begin testing, open the spec file (\src\app\app.component.spec.ts) and import AppComponent.

import { AppComponent } from './app.component';

Add some scaffolding.

describe('AppComponent', () => {

  it('should increment the counter', fakeAsync(() => {
    // Arrange
    // Act
    // Assert
  }));
});

We’re using the AAA approach to testing – Arrange, Act, Assert.

Arrange

To start testing, create an instance of the AppComponent class.

 let appComponent = new AppComponent();

You also need to initialise the counter.

appComponent.counter = 5;

Act

Lets do it… lets call the method you are trying to test.

appComponent.plusOne();

Assert

Did that work? Lets check.

 expect(appComponent.counter).toBe(6);

This is Jasmine syntax. This line is saying “I expect the counter to be 6”.

That was easy. Here’s the full code.

it('should increment the counter', fakeAsync(() => {
  // Arrange
  let appComponent = new AppComponent();
  appComponent.counter = 5;

  // Act
  appComponent.plusOne();

  // Assert
  expect(appComponent.counter).toBe(6);
}));

Paste it into the .spec.ts file and save the file.

Switch back to console. You should see 1 of 1 test passed.

Testing a component with injected dependencies

Lets extend the sample app by injecting a service into our component.

The service is just a wrapper around the @angular/http module. It retrieves customers from a REST endpoint.

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';

@Injectable()
export class CustomerService {

constructor(public http: Http) {
}

getCustomers(): any {
  return this.http.get("/customers").toPromise()
    .then(result => {
      // Success
      return result.json();
    })
    .catch(reason => {
      // Error
      console.log(reason);
    });
  }
}

Save the code in \src\app\customer.service.ts.

Open the component file. Import the service.

import { CustomerService } from './customer.service';

Inject the service.

constructor(public customerService: CustomerService) {

Add a method to call the service.

getNumberOfCustomers(): number {
  let customers = this.customerService.get();
  return customers.length;
}

Start testing

Open the test file: \src\app\app.component.spec.ts. Scroll to the bottom and add a new test.

it('should return the number of customers', fakeAsync(() => {
  //Arrange
  //Act
  //Assert
}

Arrange

Create an instance of the CustomerService class. The CustomerService constructor takes an instance of the http object. However, you aren’t trying to test the http object so you are going to pass undefined.

let customerService = new CustomerService(undefined);

Next, you want to create an instance of AppComponent class. The AppComponent constructor takes an instance of the CustomerService object, so go ahead and pass the instance you just created.

let appComponent = new AppComponent(customerService);

You want the customerService.get() method to return a canned response so that you can test the AppComponent business logic.

You mock out the get() using the spyOn method (spyOn is a Jasmine function).

spyOn(customerService, 'get').and.returnValue(['fred', 'bob', 'sue']);

This line tells Jasmine to return [‘fred’, ‘bob’, ‘sue’] whenever customerService.get() is called.

Act

You are now ready to test your component. Go ahead and call getNumberOfCustomers().

let result = appComponent.getNumberOfCustomers();

Assert

Did that work? Lets check that 3 customers were returned.

expect(result).toBe(3);

That was easy. Here’s the full code.

// Advanced test, with a mocked http response
it('should return the number of customers', fakeAsync(() => {

  // Arrange
  let customerService = new CustomerService(undefined); 
  let appComponent = new AppComponent(customerService);

  // Mock out the getCustomers method of the customerService.
  spyOn(customerService, 'get').and.returnValue(['fred', 'bob', 'sue']);

  // Act
  let result = appComponent.getNumberOfCustomers();

  // Assert
  expect(result).toBe(3);
}));

Paste it into the .spec.ts file and save the file.

Switch back to console. You should see 2 of 2 tests passed.

Summary

In this article you’ve seen how easy it is to test angular. The angular documentation refers to these as isolated tests.

However, it’s often more productive to explore the inner logic of application classes with isolated unit tests that don’t depend upon Angular. Such tests are often smaller and easier to read, write, and maintain.

In my experience, this approach is all you need to test an angular app.

Angular provides the TestBed class. TestBed provides lots of useful, advanced tools, but I’ve rarely needed to use it.

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.