Best practices in test automation with Cypress
Tags: testing, cypress, e2etesting, bestpractices
Published: 2021-09-27 05:50:01

On July 31, 2021, I had the pleasure of participating in the [**Minas Testing Conference**](https://minastestingconference.com.br), where I spoke about [**How to create fast and robust automated tests with Cypress**](https://youtu.be/iXFVV76YnRQ?t=106).

**Note:** The lecture was in Portuguese.

The talk was really cool, and I brought you a summary with each of the best practices, so you can apply it to your projects and make your tests faster and more robust.

**Note 2:** You can apply most of the best practices mentioned below regardless of the test framework being used, but some are specific to Cypress.

**1. Independent tests**

Automated tests must run in isolation, without the need for another test to run to create some state in the application under test.

**_The failure of one test shouldn't impact the results of other tests._**

In addition, independent tests are easier to parallelize.

**Tip:** use the `beforeEach` hook when you need to perform repeated steps for all tests of a given `describe` or `context`.

**2. Programmatic authentication**

Logging in via the graphical user interface as a pre-condition for all tests is costly (in terms of execution time), and it makes the tests dependent on each other, which is a bad practice.

A disabled login button due to an HTML error, for example, shouldn't break an entire test suite.

By adding [**mechanisms to log in programmatically**](https://docs.cypress.io/api/commands/session), you make tests faster and more independent.

**3. State creation mechanisms**

By creating such mechanisms, we ensure that tests are completely decoupled from each other, we don't need abstractions that add complexity to tests, such as Page Objects, and we guarantee fast and straight-to-the-point tests.

Examples of these mechanisms are API calls for creating resources, communication with the database via tasks, or running scripts at the operating system level.

**4. `data-*` attributes**

Adding attributes to front-end elements, such as `data-test`, `data-testid`, or `data-cy`, adds to the application what I call "**testability**," as these attributes are specifically created for testing purposes, decreasing the chances of front-end changes break the tests.

**5. Don't do this: `var el = cy.get('selector')`**

Remember. Cypress is not Selenium! 😅

Cypress has its own architecture, where although in many cases it allows writing code that seems synchronous, even if it is asynchronous (since it puts each command in a queue for later execution), it is not possible to do something like:

```js
const myBtn = cy.contains('button', 'My button')
myBtn.click()
```

However, you can do:

```js
cy.contains('button', 'My button').as('myBtn')
cy.get('@myBtn').click()
```

**_Most of Cypress's commands are chainable!_**

**6. Do not test external applications**

Relying on Google's GUI login for your tests or some third-party API can make your tests unstable, as changes to these services (which you don't control) will break your tests, even if it's all right "on your side."

To ensure that such services are in line with your application, you can have a smoke-test suite, for example, or contract tests.

**7. Tests too small or too large**

End-to-end tests are not unit tests. Due to the cost of running them, it's worth adding more than one assertion per test to save time.

However, beware of extensive tests. Maybe these are testing a lot of unrelated things and could be broken down into smaller ones.

**8. Do not use the `after` and `afterEach` hooks**

If something goes wrong during the execution of the tests, such hooks run the risk of not being executed, leaving "junk" in the application.

As a good practice, do any cleanup before running the tests, using the `beforeEach` hook, for example.

**9. Do not use `cy.wait(Number)`**

Cypress already has automatic waiting with different default timeouts to wait for elements to be visible, for animations to end, for requests to be sent and responded and for pages to be loaded.

What you can do to make your tests even more robust is wait for elements to be visible:

E.g.: `cy.get('[data-cy="avatar"]').should('be.visible')`

Or, you can wait for a particular request that you gave an alias to finish.

E.g.:

```js
cy.intercept(...).as('myReq')

...

cy.wait('@myReq')
```

I wrote about it on [**`cy.request` vs. `cy.intercept`**](https://dev.to/walmyrlimaesilv/cy-request-vs-cy-intercept-cmi).

**10. `start-server-and-test`**

Use mechanisms such as the [**`start-server-and-test`**](https://www.npmjs.com/package/start-server-and-test) library to initialize the application server before running the tests, ensuring that it will run with the correct code and that the tests will only run when the application is responding.

Besides, such an approach will help you run your tests on a [**Continuous Integration**](https://docs.cypress.io/guides/continuous-integration/introduction) server.

**11. Set the `baseUrl`**

By setting the `baseUrl` in the configuration file (`cypress.json`), your tests can visit pages via relative URLs, making them cleaner.

In addition, the `baseUrl` can be overwritten via a `cypress.env.json` file, environment variables (`CYPRESS_*`), command line, plugins, etc., making it possible to run the same tests against different environments.

**Conclusion**

By following these best practices, you will have fast and robust tests that your team trusts and helps to maintain and evolve.