Frontend E2e Testing With Cypress

Sep 6th, 2019 - written by Kimserey with .

Cypress is a frontend testing tool which can be used to write unit test and end to end (e2e) tests. It comes packed with features that make it easy to write tests, execute them and trace back failures. Today we’ll look at how we can setup Cypress and write e2e tests for a SPA application.

In this post, we will be writing an example test suite which will ensure that the routes from our application are accessible as expected. Our application will be a SPA application containing three routes, a landing route, a route with a parameter and a protected route.

Install Cypress

Cypress can be installed directly from NPM, therefore we simply install it using:

1
npm install cypress --save-dev

Once installed, we get a cypress executable which we can run with npx but we can set it up as a NPM script by adding the commands in package.json:

1
2
3
4
"scripts": {
  "cy:open": "npx cypress open",
  "cy:run": "npx cypress run"
}

Then we can either open the Cypress console:

1
npm run cy:open

Or directly execute Cypress CLI run command:

1
npm run cy:run

When we first installed Cypress, it created default example files with a default structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
/cypress
  /fixtures
    example.json
  /integration
    /examples
      actions.spec.js
      ...
  /plugins
    index.js
  /support
    command.js
    index.js
cypress.json

We can use these examples to first test if Cypress is working as expected by clicking Run all specs on the Cypress console. This will run all the specs found under the /integration folder. If it is installed properly, it will open a browser and start running all automated tests. We can also run a specific spec by browsing and clicking on one particular spec. Else we can simply use npm run cy:run to get a silent run from the CLI.

In this post we will touch the following files and folders:

  • /integration is the folder where we will place our specs to be run by Cypress,
  • /fixtures is the folder where we will place stub data to be used during our testing,
  • /support/command.js is a file used to extend Cypress functionality with new commands,
  • cypress.json is a configuration file used by Cypress to set global configurations.

Write our Tests

Before starting writing our test, we can set the configuration file of Cypress cypress.json:

1
2
3
4
{
    "baseUrl": "http://localhost:4200",
    "video": false
}
  • baseUrl will be used when visiting pages of our site. We assume that our application will be available on http://localhost:4200,
  • video: false instructs Cypress to not record mp4 videos of our runs.

The API of Cypress being restrictive enough, we will not be defining variables, not be creating interfaces, classes, etc.. we will mainly combine retrieval of elements and assertion therefore we will be writing our specs in JS. But we can still make use of the types defined by Cypress using:

1
/// <reference types="Cypress" />

To write test, Cypress leverages well known libraries; Mocha for the overall test structure and Chai for the assertions.

The overall structure of a test will then be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <reference types="Cypress" />

context('Application routing', () => {
  it("has a title containing 'Welcome'", () => {
    cy.server()
      .route('http://localhost:5000/heroes', 'fx:heroes');

    cy.visit('/');

    cy.get('h1')
      .should('contain', 'Welcome');
  });
  
  it("has a title containing Superman", () => {
    cy.server()
      .route('http://localhost:5000/heroes/*', 'fx:hero');

    cy.visit('/heroes/1');
      
    cy.get('h1')
      .should('contain', 'Superman');
  });
});

Cypress console

context is provided by mocha, meant to provide some context about the tests we are writing while it defines the test itself. Within each test, we use the global variable cy to run commands. There are many commands available and the full API documentation is available on Cypress documentation.

A simple assertion can consists of using get to retrieve a JQuery element, and using should do make an assertion on the element. If we simply need to ensure that an element is present in a page, we can use get as it will fail the test if the element can’t be retrieved.

For example in our first we assert the following:

1
2
cy.get('h1')
  .should('contain', 'Welcome');

Cypress will find a h1 title and make sure that it contains Welcome as part of its text. contain is a chai assertion, the type file provided by Cypress node_modules/cypress/types/index.d.ts contains the list of the possibility in the interface Chainer<Subject>:

  • be.above
  • be.below
  • be.true
  • be.lt (be lower than)
  • be.lte (be lower than or equal to)
  • exists
  • eq
  • include
  • match
  • etc..

Prior to asserting the title, we need to visit the page we want to assert. We can do that using the visit command:

1
cy.visit('/')

We can omit the base url as we configured it earlier in the cypress.json. Since Cypress executes the javascript, it will make the calls to your server. We can also mock the endpoints if needed:

1
2
cy.server()
  .route('http://localhost:5000/heroes/*', 'fx:hero');

server command starts the mock server and route command is used to setup a particular route to mock. We can chain multiple routes to be mocked. The route command takes a blob pattern for the URL, and the value of the fixture to be provided. fx:[fixture] is a shortcut to the command cy.fixture(...) which could also be used to retrieve fixtures define in the /fixtures folder.

We then end up with the following completed test:

1
2
3
4
5
6
7
8
9
it("has a title containing 'Welcome'", () => {
  cy.server()
    .route('http://localhost:5000/heroes', 'fx:heroes');

  cy.visit('/');

  cy.get('h1')
    .should('contain', 'Welcome');
});

Frontend code being inherently asynchronous, due to data being loaded asynchronously and animations. Cypress adapts itself to that by having all its command;

  1. asynchronously executed,
  2. having a build in retry mechanism.

In our second test,

1
2
3
4
5
6
7
8
9
it("has a title containing Superman", () => {
  cy.server()
    .route('http://localhost:5000/heroes/*', 'fx:hero');

  cy.visit('/heroes/1');
    
  cy.get('h1')
    .should('contain', 'Superman');
});

We are testing that the header contains the name of the superhero loaded from the page. The superhero data would be retrieved from a call to our server to http://localhost:5000/heroes/* which is asynchronous.

Cypress handles that by having all its command executed asynchronously but in order of how they are defined. Therefore, visit will run after server and route are defined, and get will run after the visit is triggered. While the visit is occurring, get will be retried. After the loading, get will succeed or after a timeout preset, it will consider the test as a failure. This provides a deterministic and reproducible execution of tests which is highly important when doing testing in general.

Because commands are retried, a smart mechanism is built into Cypress and we need to be aware of the different gotchas, summarized on their documentation. One of them being that only the last command is being retried. Therefore it is preferable to merge requests like

1
2
cy.get('h1')
  .find('a')

into a single

1
cy.get('h1 a')

or we can also alternate assertion and commands

1
2
3
4
cy.get('h1')
  .should('exists')
  .find('a')
  .should('exists')

Debugging Tests

When using Cypress console, debugging is very easy as we have access to the developer console of the browser. We can make use of logs or breakpoint to breakpoint somewhere in our tests:

1
2
3
4
5
6
7
8
9
10
11
it("has a title containing Superman", () => {
  cy.server()
    .route('http://localhost:5000/heroes/*', 'fx:hero');

  cy.debug();

  cy.visit('/heroes/1');
    
  cy.get('h1')
    .should('contain', 'Superman');
});

cy.debug() allows us to place a breakpoint in the test, equivalent of putting debugger.

Another nice feature comes from Mocha allowing us to skip a test:

1
2
3
it.skip("has a title containing Superman", () => {
  //...
});

Or only run a test to zero in a particular test:

1
2
3
it.only("has a title containing Superman", () => {
  //...
});

And that concludes today’s post!

Conclusion

Today we saw how we can get started in testing our frontend application by writing end to end tests using Cypress. We started by looking how we could install Cypress then we moved on how we could write ou first tests and we completed this post by looking at how we could debug tests and look into problems. Hope you liked this post and I see you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.