Automated Testing in Angular: End to end testing with CypressAuthor: Gergely Bikki

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

We finally arrive at the most complex, expensive and and not-so-maintainable automated tests: end-to-end tests. If you are familiar with Angular, you have probably heard of Protractor and its negative aspects.

Well, good news: we are going to to start by stashing Protractor.

npm uninstall protractor
rm -rf e2e
          

But wait, you mentioned Cypress in the title. Why is it a better solution?

Protractor is a great tool, but it can make your most energetic day your worst nightmare. Literally. It is very hard to debug, even harder to make robust test cases and also introduces more complexity to the already expensive tests. Protractor works like a machine when running end-to-end test cases and most problems stem from this.

The runner needs to simulate the behavior of a real user. The user would retry if something goes wrong, or gracefully wait for a spinner to complete. This is where Cypress really shines.

Project overview

We are going to use the same example from part 3 for demonstrating how an end-to-end 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();
  }
}

We are going to create an end-to-end test for the complete behaviour: pressing the Load button and then assert that the list of users appear correctly.

Installing Cypress

Adding Cypress to your project (let it be a new or existing one) can be easily done by running the following command:

npm install cypress —save-dev
          

You will also need to add the following entry to your scripts section in package.json:

{
  "scripts": {
    "cypress": "cypress open"
  }
}

If you execute npm run cypress, Cypress will create the folder structure for your e2e tests in the project root directory. The directory cypress will have 4 sub-directories in it:

  • fixures: containing test data as json files
  • integration: end-to-end tests/spec files
  • plugins: hook for Cypress plugins
  • support: page objects and Cypress commands

As you have probably already noticed, Cypress has added couple of example files, so we need to remove everything from fixtures and integration for now. We are going to start with a clean slate.

Creating our first e2e test

The time has finally come, we are going to have an overview of how a smoke test created with Cypress is going to look like.

Let's create a users.json file in the fixtures folder. This is going to house our response for the API request launched in the service. (Note, that we could also use a real server for this instead of using request interceptors.)

[
  { "name": "Test 1", "email": "test1@test.com" },
  { "name": "Test 2", "email": "test2@test.com" },
  { "name": "Test 3", "email": "test3@test.com" }
]
  

I'm a huge fan a logic abstraction so we are going to make an architectural decision first: we won't interact with the Cypress framework directly from the test files. Instead we are going to create page objects, which will abstract away this kind of logic.

We are going to have a users.po.ts file in the support folder containing:

/// <reference types="cypress" />

export class UsersPage {
  public static visitPage() {
    cy.visit('http://localhost:4200/users');
  }

  public static setupRequestInterceptors() {
    cy.server();
    cy.route('https://jsonplaceholder.typicode.com/users', 'fixture:users.json');
  }

  public static clickLoad() {
    cy.get('#load').click();
  }

  public static getNumberOfUsers() {
    return cy.get('li').its('length');
  }
}

We have wired up four different methods here for interacting with the test framework:

  • visitPage(): this is going to instruct Cypress to open the /users path of the Angular application
  • setupRequestInterceptors(): we are setting up a pre-defined response of the users request
  • clickLoad(): we instruct Cypress to click on the Load button
  • getNumberOfUsers(): we instruct Cypress to return the number of list items

We are going to use these methods to create our test suite named users.spec.ts in the integration folder:

import { UsersPage } from '../support/users.po';

describe('UsersPage', () => {
  beforeEach(() => {
    UsersPage.visitPage();
    UsersPage.setupRequestInterceptors();
  });

  it('should load users', () => {
    UsersPage.clickLoad();

    UsersPage.getNumberOfUsers().should('equal', 3);
  });
});
          

...and that's it. The test suite is very straightforward, possibly even more easily understandable than integration tests.

Okay, how do I run this?

You need to open two terminal windows. The first is going to house the Angular application, the other terminal the Cypress runner.

Terminal 1:

npm start
          

Terminal 2:

npm run cypress
          

If you did everything right, the Cypress runner should inform you that our suite has been successfully concluded. And it wasn't even complicated :)

This is were Cypress really shines: it turns complex and time consuming end-to-end test development into a cakewalk.

Summary

In the beginning I mentioned that Cypress makes end-to-end testing easily maintainable. The test suite we put together is very straightforward, we don't have to account for retry actions, anything low-level or DOM interactions. Cypress handles this for us.

Since we are launching our application in development mode (npm start), this approach is not suitable for CI/CD pipelines and automation. However, with a little trick we can easily cover this too and we are going to discuss this in the next and last post of this series.