Automated Testing in Angular: Integration testsAuthor: Gergely Bikki

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

So previously we have taken a look at multiple approaches to writing maintainable unit tests. These bite-sized test cases were validating individual business logic sections, however they do not provide an overview of how well these individual sections are playing together.

This is where integration testing comes into play: we are going to test whole workflows of the application. There is a good chance that we have already created unit test cases for the individual steps in the workflow, but we don’t know if these parts function communicate correctly.

Fundamental difference

Previously we did not allow any dependencies when writing unit tests. We created stubs, spies, mocked services and checked how the tested logic communicates with these.

For integration tests we are going to allow particular dependencies and child components that are part of the workflow. This includes services (along with their business logic), presentational components and anything else that’s related to the workflow. However, we still want to mock external dependencies like API or I/O communication etc. as this is a trait of end-to-end tests (which we are going to take a look at in a future post).

Structural setup of an integration test

We are going to use the same example from part 2 for demonstrating how an integration test is created. This workflow consists of acquiring a list of users from an API and then displaying them.

Our basic service for acquiring the users looks like this:


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

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');
  }
}

The display component looks like this:


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();
  }
}

The workflow consists of the following steps:

  • the user clicks on the Load button
  • the service acquires the list of users from the API
  • the component displays the list of users

We have to create an integration test that these 3 steps function correctly. We are only allowed the mock the API response, but not the business logic inside the service.

Creating the integration test

First we are going to proceed by creating a testing suite and setting up Angular’s testing environment using TestBed. We need to add our presentational component (UserComponent), service (UserService) and the HttpClient testing module for handling API requests.


import { async, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { UserService } from '../services/user.service';

describe('Integration: UsersComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      declarations: [UsersComponent],
      providers: [UserService]
    }).compileComponents();
  }));
});

          
          

Quite straightforward so far. So now we have our environment set up correctly for the testing scenario.

Next we need to validate that the TestBed is indeed able to instantiate our component by adding a test case.

import { async, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { UserService } from '../services/user.service';

describe('Integration: UsersComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      declarations: [UsersComponent],
      providers: [UserService]
    }).compileComponents();
  }));

  it('creates the component', () => {
    const fixture = TestBed.createComponent(UsersComponent);
    expect(fixture.componentInstance).toBeTruthy();
  });
});

This a must step in every test suite.

Now the real deal: we have to create the test case for testing our workflow. As mentioned previously, we have to interact with the component to press the Load button, then validate that there is indeed an API request going out (which we need to provide dummy data for) and then check whether the correct number of list items have appeared.

import { async, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from '../services/user.service';

const dummyUsers = [
  { name: 'Test User', email: 'test1@test.com' },
  { name: 'Test User2', email: 'test2@test.com' }
];

describe('Integration: UsersComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      declarations: [UsersComponent],
      providers: [UserService]
    }).compileComponents();
  }));

  it('creates the component', () => {
    const fixture = TestBed.createComponent(UsersComponent);
    expect(fixture.componentInstance).toBeTruthy();
  });

  it('loads the users and displays them if user clicks on button', () => {
    // acquire services and set up spies
    const fixture = TestBed.createComponent(UsersComponent);
    const userService: UserService = TestBed.get(UserService);
    spyOn(userService, 'get').and.callThrough();
    const httpController: HttpTestingController = TestBed.get(HttpTestingController);

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

    // validate outgoing request to API and provide dummy data
    expect(userService.get).toHaveBeenCalled();

    expect(fixture.componentInstance.users$).toBeTruthy();

    fixture.detectChanges();

    const testRequest = httpController.expectOne('https://jsonplaceholder.typicode.com/users');
    expect(testRequest.request.method).toEqual('GET');
    testRequest.flush(dummyUsers);

    // validate presentational changes
    fixture.detectChanges();

    const listItems = fixture.nativeElement.querySelectorAll('li');
    expect(listItems.length).toEqual(dummyUsers.length);
  });
});

Now we have a working test suite for one of the workflows in our application. We can immediately see the different between unit tests and integration tests - how expensive it is to create, how complex they are and why we should not overuse them.

Summary

In this week’s post of this series we have gained some insight into the traits and structure of integrations tests. In the following parts we will see how to swap the built-in e2e technology (Protractor) for a better alternative and create even more complex tests using Cypress.