DEV Community

Eduardo Henrique Gris
Eduardo Henrique Gris

Posted on

E2E testing in React with Cypress

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
    1. access the page where the test will start
    2. search an HTML element on the screen
    3. interact with the element
    4. 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", () => {
    (…)
  })
})
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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',
  },
})
Enter fullscreen mode Exit fullscreen mode

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',
  },
})
Enter fullscreen mode Exit fullscreen mode

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()')
  })
})
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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:

Image description

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:

Image description

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!:

Image description

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

Image description

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:

Image description

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:

Image description

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:

Image description

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:

Image description

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:

Image description

In the case of execution via the interface, it will show specifically where the test failed, with the image displayed alongside it:

Image description

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')
  })
})
Enter fullscreen mode Exit fullscreen mode

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:,
  },
})
Enter fullscreen mode Exit fullscreen mode

Use beforeEach for shared code between tests

It is recommended to place shared code inside a beforeEach:

beforeEach(() => {
// shared code
})
Enter fullscreen mode Exit fullscreen mode

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()')
})
Enter fullscreen mode Exit fullscreen mode
// Recommended
it('has queries options', () => {
  cy.get('[data-cy="queries"]').should('contain', 'get()')
                               .and('contain', 'within()')
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)