Introduction
In the article from september, I wrote about how to set up Cypress to enable end-to-end testing in React, demonstrating two ways to run the tests (through the interface and via the terminal). Now, the idea is to show how the testing works, along with some concepts and examples.
Tests structure
The test structure follows this format:
- describe: represents the test block to be executed, which can refer to a specific context or flow
- it: represents the test to be run, following these steps
- access the page where the test will start
- search an HTML element on the screen
- interact with the element
- define an assertion for the test
describe("Block description", () => {
it("test description", () => {
access the page
search for an element on the screen
interaction with the element
test assertion
})
it("another test description", () => {
(β¦)
})
})
Here's an example of a search, an element interaction and an assertion inside the test, but it's possible to perform multiple ones inside it.
Access the page
To access a page, cy.visit()
will be used, which allows navigating to a URL that will serve as the starting point for the tests.
Search for an element on the screen
There are various ways available for searching HTML elements. Here are some examples:
Query | Search | Code |
---|---|---|
contains | by text | contains(text) |
get | by selector | get(selector) |
The search using get
can be done in various ways:
Search | Example |
---|---|
by data-cy | cy.get('[data-cy="example"]') |
by name | cy.get('[name="submit"]') |
by id | cy.get('#main') |
by className | cy.get('.btn') |
by role | cy.get('button') |
The data-cy
corresponds to a definition on the element that is exclusive for locating it in tests.
Interaction with the element
After locating the element on the page, itβs possible to interact with it. Here are some available ways:
Interaction | Action |
---|---|
click() | click |
dblclick() | double click |
type() | type a text |
check() | check checkbox/radio button |
uncheck() | uncheck checkbox |
submit() | submit a form |
Test assertion
To define the test assertion should()
will be used, which has an alias and()
. Here are some available ways:
Assertion | Validation |
---|---|
should('have.value', value) | value |
should('be.enabled') | is enabled |
should('be.disabled') | is disabled |
should('contain', text) | text |
should('have.length', length) | length |
should('exist') | existence |
should('include') | inclusion |
To validate the negation of assertions, not.
can be placed as the first part inside should
, for example should('not.exist')
.
Tests execution
In last month's article, linked in the introduction of this article, two ways to run tests in package.json
were defined, via the interface and through the terminal:
{
"scripts": {
//...
"cy:open": "cypress open", // interface
"cy:run": "cypress run" // terminal
}
}
To run tests locally in Cypress, the application must first be started locally in one terminal and then run the tests in another terminal.
Since the same port is defined when the app is started locally, and to avoid always writing the same starting point in the first step of all tests (for example, starting from port 3000), itβs possible to define this in cypress.config.js
:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
},
})
This way, any test that starts with cy.visit('/')
will be directed to the page http://localhost:3000/
.
Observation: when running tests in testing environment pipelines, an ENV
variable can be set in baseUrl
to access the expected url associated with the environment where the tests will be executed.
Tests example
For the following examples, it will be used the sample page that Cypress itself provides: https://example.cypress.io
That will represent, for this article, the url corresponding to the application running locally in the terminal.
Since all examples will start from this url, it will be set as the baseUrl in cypress.config.js
:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'https://example.cypress.io',
},
})
The idea is to provide simpler examples to demonstrate each step involved in writing tests. However, in practice, when Cypress is used for end-to-end testing validation, the test tend to be more complex and involve many more interaction inside them.
Presence test
In this first test, it will be checked if, after clicking a link on the Cypress example page, we arrive at the expected page (making an assertion on the url) and whether certain elements are present on the screen.
First, it will be placed the file inside the cypress/e2e
folder, which is where Cypress looks for tests to execute, naming the file presence.cy.js
:
describe('Presence tests', () => {
beforeEach(() => {
cy.visit('/')
cy.contains('get').click()
})
it('reach expected page', () => {
cy.url().should('include', '/commands/querying')
})
it('has queries options', () => {
cy.get('#querying').should('contain', 'get()')
.and('contain', 'contains()')
.and('contain', 'within()')
.and('contain', 'root()')
})
})
In this test block, the example Cypress page is accessed first, and then the link containing the text get
is clicked. Since all tests in the block start the same way, this part was placed inside a beforeEach
before the tests.
In the first test, it is expected that the page reached includes /commands/querying
in the url, and in the second test, it is expected that the different available queries are displayed on the screen, validating by their text.
Running yarn cy:run
in the terminal returns information about the test results, which I will break in parts for further explanation.
First, it shows the execution details, including the version of Cypress used, the browser, the version of node, the number of test files, and where they were retrieve from:
Next, it displays the file that was executed, the test block, and the tests that were performed (with a positive indicator showing that the test passed), along with the total number of tests that passed:
After that, a box is showed with the test results, including the total number of tests executed, how many passed, how many failed, how many were pending, how many were skipped, how many generated screenshots, whether a video was recorded, the duration of the tests, and finally, the executed file:
Finally, there is a table displaying the executed file, execution time, number of tests, how many passed, how many failed, how many were pending and how many were skipped. Since all tests passed, it shows the message All specs passed!
:
Another way to run tests is via the interface, following those steps:
- Run
yarn cy:open
- Select the
E2E Testing
box - Choose your preferred browser
- Click on
Start E2E Testing
- Click on the file you want to execute
- Wait for the tests to execute
During execution, the navigation will be displayed on the right side according to the tests.
Once the execution is complete, the executed test block and the tests will appear on the left side. In this case, since they passed, there will be a positive indicator next to them. On the right side, the point where the execution stopped will be shown:
On the left side, you can open one of the tests and hover over each part that was executed inside the test to see on the right side where you were during the navigation:
Now, simulating a failure in the second test of the block by modifying one of the assertions, running it through the terminal will show some differences in the result display.
The test inside the block that failed will be highlighted in red, indicating that one test passed while another failed, along with the specific point where the failure occurred:
The results section will be highlighted in red, indicating that one test failed and that a screenshot was taken showing what the screen looked like at the moment of the failure:
Finally, a table will display that out of two tests, one passed and one failed, with a red highlight at the bottom stating 1 of 1 failed (100%)
. This information refers to how many test blocks failed, not the individual tests themselves. Since the Presence tests
block had one test failing inside it, the entire block is considered to have failed:
In the case of execution via the interface, it will show specifically where the test failed, with the image displayed alongside it:
Multiple interactions test
In this test, after clicking the type
link on the example Cypress page, it will be verified if the expected page is reached (by asserting the url), and after entering text into an input field, it will be checked whether the value inside it matches what was written.
The file will be placed inside the cypress/e2e
folder, which is where Cypress looks for tests to execute, naming the file actions.cy.js
:
describe('Actions tests', () => {
beforeEach(() => {
cy.visit('/')
cy.contains('type').click()
})
it('reach expected page', () => {
cy.url().should('include', '/commands/actions')
})
it('fill email address', () => {
cy.contains('Email address').next().type('example@aloha.com')
cy.contains('Email address').next().should('have.value', 'example@aloha.com')
})
})
In this test block, next()
is used to access the HTML element immediately following the one that was searched. This way, itβs possible to access the input field to fill it in, which is located after the searched title. After execution, the tests pass successfully.
Best practices
The Cypress documentation outlines best practices to follow during testing. I will present some of them below:
Define a global baseUrl
For tests that always start from the same domain, set the baseUrl
in cypress.config.js
:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl:,
},
})
Use beforeEach
for shared code between tests
It is recommended to place shared code inside a beforeEach
:
beforeEach(() => {
// shared code
})
Make multiple assertions together
Instead of separating assertions into different tests for the same element, make multiple assertions:
// Not recommended
it('has get query option', () => {
cy.get('[data-cy="queries"]').should('contain', 'get()')
})
it('has within query option', () => {
cy.get('[data-cy="queries"]').should('contain', 'within()')
})
// Recommended
it('has queries options', () => {
cy.get('[data-cy="queries"]').should('contain', 'get()')
.and('contain', 'within()')
})
Element Search Priority
This can be somewhat controversial, as the main suggestion is to add a unique test locator element inside the HTML element. Starting from the following component:
<button
id="example"
class="btn-primary"
name="submission"
role="button"
data-cy="primary"
>
Submit
</button>
Below is the increasing order of priority and the reasons presented in the documentation:
Selector | Recommended | Reason |
---|---|---|
cy.get('button').click() | Never | Worst - too generic, no context |
cy.get('.btn-primary').click() | Never | Bad. Coupled to styling. Highly subject to change |
cy.get('#example').click() | Sparingly | Better. But still coupled to styling or JS event listeners |
cy.get('[name="submission"]').click() | Sparingly | Coupled to the name attribute which has HTML semantics |
cy.contains('Submit').click() | Depends | Much better. But still coupled to text content that may change |
cy.get('[data-cy="primary"]').click() | Always | Best. Isolated from all changes |
Conclusion
The idea of this article was to provide an overview of how testing works, covering structure, page access, HTML element search, interactions, and validations, along with examples and best practices recommended by the documentation. However, there are many other testing scenarios and ways to use Cypress. For this reason, I am providing the main links categorized by topic for those who want to delve deeper.
Links
Writing and organizing tests
Tests execution steps
Best practices
Commands
Url access
Search using get
Interactions
Assertions
Tests execution