Write better E2E tests using Cypress while avoiding the most common pitfalls. Discusses avoiding the use of wait, importance of using baseUrl & chains, & more.
We at Tracetest take testing very seriously and have been using Cypress as our primary end-to-end testing solution for our frontend web application. The end-to-end testing solution has been working great for us; it runs as part of our CI pipeline on top of Github actions just like our own trace-based testing using Tracetest itself. Since we are a small team moving at a very fast pace, our CI pipeline is something we also take very seriously. A couple of weeks ago, while in the midst of trying to shave a couple of minutes off our pipeline total time, we decided to run our Cypress test suites in parallel. That's where the problems start to show and our implementation mistakes were made much more evident. In this article, we would like to show you how we have solved these issues as well as showcasing some of the difficulties we have encountered along the way.
## Avoid Using Wait in Cypress Tests
The first iterations of our tests rely too much on waiting for things to happen based on a given time. But this is a known anti-pattern referred to in the [Cypress documentation](https://docs.cypress.io/guides/references/best-practices#Unnecessary-Waiting). This resulted in flaky tests, making our CI pipeline not very trustworthy. Below you have an example of something pretty similar to what we were doing:
But how can we fix this properly? Cypress interceptors are here to save the day! With interceptors, as the name implies, we can intercept a request. Later in our test, we can wait for the API call you intercepted. This is incredibly useful for testing frontend applications. Frontend applications usually depend on data coming from the backend in order to render content to the users. This is exactly what we need to write much more reliable tests. The snippet should look like:
When you absolutely need to wait for a bit you can increase the timeout of certain commands. For example, we at Tracetest run our end-to-end tests against a brand new infrastructure that we deploy specifically for this purpose during the CI pipeline. Services take a little longer to be fully active, that is why it takes a longer for our JUnit results to be available so quickly. Here we take our time:
## Use a baseUrl in the Cypress Configuration File
Yet again, as the official [Cypress documentation states](https://docs.cypress.io/guides/references/best-practices#Setting-a-global-baseUrl), not setting a baseUrl on the Cypress configuration file is an anti-pattern. That creates a couple of overall benefits for the usability of the tool, such as being able to detect malfunctioning of the server before actually running any test. But, mainly to avoid an extra reload of the page whenever Cypress visits an URL making Cypress tests run smoother and faster due to avoiding the unnecessary page reload.
## Chains are Not Promises
All Cypress methods usually return a [**chainable** object](https://docs.cypress.io/api/commands/then). A chain is something that looks like a promise, works pretty much like a promise, but it is not a promise. This a pretty controversial decision by the Cypress team, as stated all across the web. There are libraries like **cypress-promise** that aim to match both implementations in a [pretty smart way](https://github.com/NicholasBoll/cypress-promise/blob/master/index.js#L3-L20), enabling you to use to use aync/await js feature. But in our experience, it did not work very well here at Tracetest. We saw some weird behavior where the test would succeed even if it wasn’t finished yet. Our tip here would be: embrace the Cypress way. Use chains. Even though code looks a bit awkward by having to chain methods with **.then()** calls; this is the best you can get currently:
## Abstract Calls into Custom Methods
Cypress code is intrinsically hard to read. It looks like some random jQuery-like selector spread around making it very hard to understand it when reading it for the first time. Even if you are familiar with the codebase, it can still be difficult to understand Cypress code. How can we solve this issue? We can write [custom Cypress commands](https://docs.cypress.io/api/cypress-api/custom-commands) that can be used across all tests. If you are using typescript, you can make use of editor intelliSense in order to easily reuse those commands when writing new code. But, how did we achieve this? By overwriting Cypress's Chainable interface. This way we can add those commands to the cy object itself. Pretty cool, right?
If you abstract all methods you usually use, the code that uses them will be much easier to read and understand. Below, you can see how incredibly easy it is to read this **createTest** method that is making use of lots of custom methods. Anyone can read the method name and quickly associate what these couple of lines of code are trying to accomplish.
## Conclusion - Getting Testing Right is Hard!
These are some of the improvements we managed to implement in the Tracetest repository. Not to say this refactoring was easy. Cypress testing is a very delicate piece of code. Small changes on the testing code can significantly change a test’s behavior. In order to make all these changes work properly, we had to spend a lot of time making it right. We hope you can also take the time to invest in your code testing capabilities. We have several other blog posts discussing how we have improved our tests which you may find interesting:
- [Integrating Tracetest with GitHub Actions in a CI pipeline](https://kubeshop.io/blog/integrating-tracetest-with-github-actions-in-a-ci-pipeline)
- [Detect & Fix Performance Issues Using Tracetest](https://kubeshop.io/blog/detect-fix-performance-issues-using-tracetest)
- [Integration Tests: Pros and Cons of Doubles vs. Trace-Based Testing](https://kubeshop.io/blog/integration-tests-pros-and-cons-of-doubles-vs-trace-based-testing)
We encourage you to [use Tracetest](https://github.com/kubeshop/tracetest), our Open Source testing solution which utilizes the power and visibility exposed by OpenTelemetry tracing. Tracetest complements Cypress tests to allow developers to increase the observability of their code and create powerful backend integration tests. Feel free to join [our Slack community](https://dub.sh/tracetest-community) to discuss Tracetest or any of the content in this article. Happy Testing.
Learn how to create setup and teardown of trace-based tests with Test Suites, allowing for codeless setup to chain together several tests into one comprehensive flow!