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