Automated Testing in Angular: Unit testsAuthor: Gergely Bikki

This is the second post in the Automated Testing in Angular series:

In the previous, introductory post we have taken a brief look at the traits of different types of automated tests. Now we are going to take a deeper dive into the nature of one of these: unit tests.

Unit tests are bite-sized, isolated scripts aiming to validate the behaviour of individual blocks in the application. We usually aim to write unit tests only for essential parts of the application (eg. crucial business logic, container components) and avoid testing presentational logic. Angular provides excellent tools for writing modern and maintainable unit tests.

Our example component

Let’s create a small counter component that increments a counter every time a button is pressed.

@Component({
  selector: 'app-counter',
  template: `
    <h3>Counter</h3>

    <span>Counter state: {{ counter }}</span>

    <button id=“increment” (click)="increment()">Increment</button>
  `
})
export class CounterComponent {
  counter = 0;

  increment() {
    ++this.counter;
  }
}
          

As you can see a component comprises of a template and the component logic itself. It is not mandatory to include the template for a unit test since eg. a service containing business logic also has no template.

Template-less unit testing

Let’s take a look at how a basic, template-less unit test suite would look like for our example. We are grouping the individual test cases using a behaviour-driver approach (Behaviour Driver Development) to keep it clean and easily readable.

Before running any of the test cases, we are setting up the state of the component for the individual test cases in the beforeEach hook. This usually involves component instantiation and setting up/resetting any component properties or spies.

describe('CounterComponent', () => {
  describe('when increment action is executed', () => {
    let component: CounterComponent;
    let oldCounter: number;

    beforeEach(() => {
      component = new CounterComponent();
      oldCounter = component.counter;

      component.increment();
    });

    it('increments counter by one', () => {
      expect(component.counter).toEqual(oldCounter + 1);
    });
  });
});
          

Shallow unit testing

However, we all know that the Angular component truly is the template and the class working together. We ideally want to include the template too when conducting unit tests. We can do this using a technique called shallow unit testing.

Shallow unit testing is an excellent choice for testing our component (and only that particular component) while ignoring all the child templates. We don’t want to render presentational components and test them.

For this purpose we can use Angular’s TestBed. Its primary purpose is to set up a working testing environment so we can leverage various features of the framework like dependency injection.

We need to tell the TestBed that we don’t want to render any child components by setting the schemas option to NO_ERRORS_SCHEMA.

describe('CounterComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [CounterComponent],
      schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
  });

  it('creates the component', () => {
    const fixture = TestBed.createComponent(CounterComponent);

    expect(fixture.componentInstance).toBeTruthy();
  });

  describe('when increment button is clicked', () => {
    let component: CounterComponent;
    let oldCounter: number;

    beforeEach(() => {
      const fixture = TestBed.createComponent(CounterComponent);
      component = fixture.componentInstance;

      oldCounter = component.counter;

      const button = fixture.nativeElement.querySelector('#increment');
      button.click();
    });

    it('increments counter by one', () => {
      expect(component.counter).toEqual(oldCounter + 1);
    });
  });
});

This is a more complex test suite, however it allows us to interact with the component’s template using the built-in element selector methods.

We are first checking whether the TestBed is able to instantiate our CounterComponent. After that we are using the TestBed to acquire an instance of the component before every test case. And after that we do the interaction with the template and finally check the results of our actions.

Mocking out service dependencies

Mocking out dependencies of components and services is a must in order to have efficient unit tests. As mentioned earlier, the TestBed is capable of interacting with the dependency injection system.

Let’s create a separate example for this section where we are fetching a bunch of users from an API.

We will start by creating the service for API interaction purposes:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface User {
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  get(): Observable<User[]> {
    return this.http.get<User[]>('https://jsonplaceholder.typicode.com/users');
  }
}

Next we will have our component that presents us the list of users when the Load button is pressed:

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { User, UserService } from './user.service';

@Component({
  selector: 'app-users',
  template: `
    <h3>Users</h3>

    <ul *ngIf="users$">
      <li *ngFor="let user of users$ | async">{{ user.name }} ({{ user.email }})</li>
    </ul>

     <button id="load" (click)="load()">Load</button>
  `
})
export class UsersComponent {
  users$: Observable<User[]>;

  constructor(private userService: UserService) {}

  load() {
    this.users$ = this.userService.get();
  }
}

For the unit test we are going to use the built-in Spy feature of the Jasmine framework. Using this feature we can create spy functions that act as stubs for the real methods and are capable of tracking invoked actions, arguments and many more. In this example we are going to set up the return value of the method and later assert whether the function has been called.

We are going to proceed with the test suite in the following order:

  • Create a spy for the get() method of the service.
  • Create a stub object for the UserService class using the TestBed DI system. We are going to utilize the previously created spy as a replacement method for the original UserService.get() function.
  • Create the test cases and set up the spy as part of the beforeEach hook.
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { UsersComponent } from './users.component';
import { UserService } from './user.service';
import { of } from 'rxjs';

const listItems = [
  { name: 'abcd', email: 'test@test.com' },
  { name: '321', email: 'test1@test.com' },
  { name: 'test', email: 'test2@test.com' }
];

describe('UsersComponent', () => {
  const getUsersSpy = jasmine.createSpy('get');

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [UsersComponent],
      providers: [
        {
          provide: UserService,
          useClass: class {
            get = getUsersSpy;
          }
        }
      ]
    }).compileComponents();
  });

  it('creates the component', () => {
    const fixture = TestBed.createComponent(UsersComponent);
    fixture.detectChanges();

    expect(fixture.componentInstance).toBeTruthy();
  });

  describe('when load button is clicked', () => {
    let userService: UserService;
    let fixture: ComponentFixture<UsersComponent>

    beforeEach(() => {
      fixture = TestBed.createComponent(UsersComponent);
      userService = fixture.debugElement.injector.get(UserService);
      getUsersSpy.and.returnValue(of(listItems));

      const button = fixture.nativeElement.querySelector('#load');
      button.click();

      fixture.detectChanges();
    });

    it('loads users from UserService', () => {
      expect(userService.get).toHaveBeenCalled();
    });

    it('creates list items', () => {
      const listItemElements = fixture.nativeElement.querySelectorAll('li');
      expect(listItemElements.length).toEqual(listItems.length);
    });
  });
});

Summary

In this article we have examined how to create well-maintainable unit tests using different approaches and techniques. Unit tests really shine because of their cheap and valuable nature and are a must for any serious project. In part 3 we will take a look at the more complex and expensive integration tests.