DEV Community

Cover image for Cypress Super-patterns: How to elevate the quality of your test suite
Wes Copeland
Wes Copeland

Posted on

Cypress Super-patterns: How to elevate the quality of your test suite

Cypress is awesome and a game-changer for testing your apps. You get the tools you need out of the box to be instantly productive and ship your code with confidence. Over the last year, I have relied on Cypress heavily for multiple industry projects and several hobby projects, and it has elevated my confidence to ship new code through the stratosphere.

However, I don't follow the golden path when using Cypress. I use a couple of plugins that genuinely make a huge difference for productivity. In this article, I will share my approach to Cypress testing and how you can use it to take your tests to the next level.


🤷‍♂️ Why Cypress?

You're likely already using Jest or Karma as a test runner for your unit tests. Without going too deep, unit tests are great and you should still write them. At best, they verify the correctness of your components and services under deliberately isolated use cases. At worst, they are testing implementation details to turn some green lights on.

To understand why Cypress is a great tool, it is helpful to first understand the Testing Trophy. This is Kent C. Dodds' compelling front-end take on the traditional testing pyramid:

The testing trophy

The trophy boils down a comprehensive front-end testing strategy to four levels:

  • Static Analysis. This includes tools like Prettier, ESLint, and even TypeScript. All of these are raising the floor of your code quality before code is even committed to the codebase.
  • Unit Testing. Jest and Karma are the most popular tools for running unit tests in the JavaScript ecosystem. Unit tests are very quick, can be run in parallel, and verify deliberately isolated situations for correctness. When I say quick, ideally you can run a few thousand of these in under a minute.
  • Integration Testing. Jest and Karma are also great at integration testing. Cypress is my tool of choice. In an integration test, multiple units are interacting with each other and the outcome is being tested. For example, maybe you've spun up your front-end app but are using a mock back-end to verify the correctness of the UI.
  • E2E Testing. This is as close to end-user testing as we can achieve in an automated way. With E2E testing, we have a helper robot that walks through the app, hitting a real UI, a real back-end, and a real database. While these tests give us the highest confidence, they are the most expensive in terms of time and maintenance. Cypress is an excellent tool of choice for E2E tests.

The slices of the testing trophy are intentionally sized: integration tests are in the sweet spot of time and confidence.

Hopefully, at this point, it is clear that Cypress can add value to your toolchain if you're not doing any testing above the unit level.


🤷‍♀️ How do I set up Cypress?

The Cypress team has done a great job of making setup as easy as can be. The docs here should be treated as the ultimate authority, but a nice tl;dr might be:

1. Install the Cypress dependency into your project.

This can be done with a terminal command based on your package manager of choice:

npm install --save-dev cypress

OR

yarn add -D cypress
Enter fullscreen mode Exit fullscreen mode

2. Add some Cypress scripts to your package.json file.

In your package.json's scripts object, add:

"scripts": {
  ...

  "cy:open": "cypress open",
  "cy:run": "cypress run"
}
Enter fullscreen mode Exit fullscreen mode

3. Start Cypress!

With Cypress installed and your commands added, you're ready for liftoff! In a terminal, run:

npm run cy:open

OR

yarn cy:open
Enter fullscreen mode Exit fullscreen mode

On its first run, Cypress will bootstrap your project with a lot of sample tests in your project's cypress/integration folder. I do recommend deleting all of these, as we will be greatly diverging from the default way of writing Cypress tests for the remainder of the article.


Super-pattern #1: Use Cypress Testing Library

If you only implement one super-pattern, it should be this one.

The above tweet is the key guiding principle of Testing Library. Testing Library is a reaction to other testing toolkits (including the default API that ships with Cypress) that they might perhaps give a developer too much power or encourage them to test things that are invisible to an end-user.

Testing Library's API is deliberately minimal to keep developers in the pit of success. Most of its exposed functionality only allows you to test software in a way it's used by real end-users. This gives you a massively increased confidence in the tests themselves and the code you ship to production, the only trade-off being you as the developer must ensure your application is actually accessible.

In the grand scheme of things, this is not much of a trade-off at all. If your app is not accessible, your app is broken.

Another benefit of using Cypress Testing Library is there's a non-zero chance you're already using another flavor of Testing Library for your unit tests. Create React App now ships with React Testing Library by default. If this is the case, the context switch of moving from your Jest tests to your Cypress tests is greatly reduced.

How to set up Cypress Testing Library

Thankfully, the setup for CTL can be completed in just a few steps. First, let's install the needed dependency:

npm install --save-dev @testing-library/cypress

OR

yarn add -D @testing-library/cypress
Enter fullscreen mode Exit fullscreen mode

Next, you will need to open cypress/support/commands.js and add the following near the top of the file:

import '@testing-library/cypress/add-commands';
Enter fullscreen mode Exit fullscreen mode

If you are using TypeScript with Cypress (which I do not recommend in any project not using Nrwl Nx due to its noticeable performance impact on test execution speed when paired with Cucumber), there are some additional steps you'll need to follow that can be found in the CTL setup docs.


🚀 Improve your Testing Library productivity!

There are a few tools and tricks you can leverage to greatly improve your capabilities with CTL. In my work, two have stuck out well above the rest.

#1 - Which query should I use?

This is your bible. It is critically important that you prioritize the correct queries to gain the full benefits of Testing Library. Note that accessible queries are the top priority because they can be seen/heard by all users regardless of any a11y considerations, whereas data attributes (which, interestingly, the Cypress docs recommend using) should be treated as the lowest priority because they are invisible and inaccessible to the end-user.

This page is strangely easy to miss in the Testing Library docs, but it's always one of the first I share with anyone just getting up to speed with any flavor of the library.

#2 - 🐸 Testing Playground Chrome Extension

This tool is a relative newcomer to the scene. Written by Stephan Meijer, this adds another tab to your Chrome DevTools that lets you select any element on the page (just like the element selector already built into the devtools) and receive the recommended Testing Library query to use!

There's an added unspoken benefit here though. If the tool cannot recommend a query, it means whatever you're pointing at is probably not accessible. This certainly removes a lot of guesswork from the equation.


Super-pattern #2: Force the Cypress timezone

Most apps, at some point, will receive datetime information from the back-end that must be displayed on the UI. Generally speaking, working with dates and times is hard and error-prone, so it's definitely something we'll want to test.

By default, Cypress uses your machine's timezone just like your web browser does. This can have unintended consequences. If your CI server (or another developer) is set to a different timezone, tests that pass on your machine will fail on others. At the time of writing, there is an open discussion on GitHub about this problem here.

Our tests should be deterministic. To achieve this with timezones, we'll force Cypress's timezone to UTC (this should match most CI providers).

We can achieve this with a very slight modification to our scripts in the package.json file:

"cy:open": "TZ=UTC cypress open",
"cy:run": "TZ=UTC cypress run"
Enter fullscreen mode Exit fullscreen mode

All done! Now no matter where your tests run, they'll simulate being in the UTC timezone.


Super-pattern #3: Use Cucumber

What the heck is Cucumber? You may also hear this referred to as "Gherkin" or "feature files".

Cucumber adds some guardrails to your Cypress tests, giving you a syntax that enables tests to very closely follow the AAA (Arrange, Act, Assert) testing pattern.

Cucumber tests live in .feature files and have a unique syntax (Gherkin) that closely mirrors the language of a user story. A Gherkin file might look like:

# HomePage.feature
Feature: Home Page

  Scenario: The Sign Up link navigates to the correct page
    Given I navigate to the home page
    When I click on the Sign Up link
    Then I should be on the Sign Up page
Enter fullscreen mode Exit fullscreen mode

With Cucumber, this is an actual test file that Cypress can execute. Each "test step" (given/when/then) maps to a re-usable function, like so:

// HomePage.steps.js
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';

Given('I navigate to the home page', () => {
  cy.visit('https://my-app.com');
});

When('I click on the Sign Up link', () => {
  cy.findByRole('button', { name: /Sign Up/i }).click();
});

Then('I should be on the Sign Up page', () => {
  cy.findByText('Sign Up now!').should('be.visible');
});
Enter fullscreen mode Exit fullscreen mode

There are numerous benefits to using Cucumber in your Cypress tests:

  • Each test step follows a UNIX-like single-purpose principle, in that they are good at doing only one thing and are authoritative in how that thing should be done.
  • Test steps are typically small and concise, making them easier to maintain.
  • Multiple tests can be built from the same set of test steps, with developers using them just like jigsaw puzzle pieces. This keeps your tests extremely DRY.
  • Test steps are reusable between feature files and integration/e2e tests!
  • Failures can be traced down to a bite-sized step function rather than a massive test method, as the steps themselves are generally stateless.

We can use Cucumber in Cypress via cypress-cucumber-preprocessor. Setup is non-trivial but well worth the effort.

The installation docs on the official GitHub repo are the best resource for how to get started. I would much rather link to these steps directly instead of carbon copy them in this article, as they are surely subject to change.

What I can share though are some productivity tips with Cucumber that are not especially obvious during the setup phase.


🚀 Improve your Cucumber productivity!

#1 - Rename the integration folder to tests.

One of the main draws of using Cucumber is we can re-use test steps across both integration and e2e tests in the same codebase. By default, Cypress names the folder all of your tests live in "integration". This makes a lot of sense for most codebases but will be suboptimal for our use case.

To make this change, you must configure it in two places: cypress.json and the cypress-cucumber-preprocessor settings in your package.json.

// cypress.json

{
  ...

  "testFiles": ["**/*.{feature,features}"],
  "integrationFolder": "cypress/tests"
}
Enter fullscreen mode Exit fullscreen mode
// package.json

{
  ...

  "cypress-cucumber-preprocessor": {
    "stepDefinitions": "cypress/tests"
  }
}
Enter fullscreen mode Exit fullscreen mode

#2 - Inside of tests, add common, integration, and e2e.

That's right, we want to add three new folders inside of our tests folder. We should name them common, integration, and e2e.

Why?

common is where shared steps between integration and e2e tests can live. cypress-cucumber-preprocessor specifically looks for this folder for global steps. For example, if you want common navigation test steps that are re-usable by any feature file, they might go in common/navigation.steps.js.

We separate our integration and e2e tests in separate folders because we likely want separate CI processes for them (integration on every PR, e2e nightly or on every merge). This folder separation will make setting that up later down the road quite a bit easier.

#3 - Enable non-global step definitions.

What about step definitions that are isolated specifically to a certain feature file? We should support this so that not every step definition is global.

This can be enabled in the package.json file:

{
  ...

  "cypress-cucumber-preprocessor": {
    ...

    "nonGlobalStepDefinitions": true
  }
}
Enter fullscreen mode Exit fullscreen mode

This might make a lot of sense for integration tests, where we have a test step for setting up a mock API response. We certainly don't want mock API response steps to be accessible in our e2e tests.

#4 - Put your i18n solution in CI mode for integration tests.

How you do this will largely depend on what front-end framework and i18n framework you're using in your project. i18next supports a "cimode" language out of the box.

What this means is integration tests will render i18n keys, whereas e2e tests hitting your actual app will show the actual i18n values. For integration testing, this can give you some confidence that you're shipping the correct i18n value regardless of the language selected by the end-user.

This can be tested in Cucumber like so:

# Integration Test
Feature: Sign Up Page

  Scenario: The heading text is visible
    Given @navigation I visit the Sign Up page
    Then @sign-up I should see the heading text "headingText"
Enter fullscreen mode Exit fullscreen mode
# E2E Test
Feature: Sign Up Page

  Scenario: The heading text is visible
    Given @i18n My language is set to "ja-JP"
    And @navigation I visit the Sign Up page
    Then @sign-up I should see the heading text "サインアップ"
Enter fullscreen mode Exit fullscreen mode
// Curly brackets let us pass dynamic data to test steps
Then('@sign-up I should see the heading text {string}', (value) => {
  cy.findByText(value).should('be.visible');
});
Enter fullscreen mode Exit fullscreen mode

#5 - Tag your global test steps.

This will save you from a massive headache down the road. Tag every test step that lives in the common folder based on the name of the file it's in. For example:

// home.steps.js

Given('@home I click the Sign Up button', () => {
  cy.findByRole('button', { name: /Sign Up/i }).click();
});
Enter fullscreen mode Exit fullscreen mode
Scenario: Sample
  Given @home I click the Sign Up button
Enter fullscreen mode Exit fullscreen mode

Now any developer who is responsible for fixing the inevitable failure doesn't have to guess whether or not any step is global, and for those that are global, they know exactly what file the step belongs to. This will also prevent naming collisions between global steps.

#6 - Add Gherkin support to VSCode.

This is the plugin you want. VSCode does not natively support Gherkin syntax, so this plugin has the potential to further elevate your productivity.

#7 - Write steps from the perspective of the user.

// 🛑 bad
Given("The button is visible", () => { ... });

// ✅ good
Given("I see the button", () => { ... });
Enter fullscreen mode Exit fullscreen mode

This is purely semantics, but I've found this helps you remain focused on the end-user experience rather than the implementation details of the page.


Thanks for reading! & additional resources

If this article was helpful at all or if you learned anything new, please leave a like! Here are some follow-up resources if you're interested in diving any deeper:

You can also follow me on Twitter, where I'm always posting or retweeting interesting things happening in the community.

Thanks for reading! 👋

Top comments (8)

Collapse
 
liviufromendtest profile image
Info Comment hidden by post author - thread only accessible via permalink
Liviu Lupei

We get a lot of new users on Endtest who moved away from Cypress.
I have been on calls with a few of them and the reason why they gave up on Cypress was because they jumped on the Hype Train without knowing that Cypress doesn't work on Safari, Internet Explorer and mobile browsers and that it does not work with multiple browser tabs.
Also, they were never able to make Cypress work with iframes.
They also didn't know that they had to use a paid service, Cypress Dashboard, in order to integrate their tests with their CI/CD system.
From what I know, if you're "open source" and "developer-friendly', your product should be free and the users should only pay for something which consumes cloud resources, but that's not the case.
Not my opinion, their opinions.

Collapse
 
wescopeland profile image
Wes Copeland

Hey everyone! If you're building a competitor to Cypress, that's great! Competition breeds innovation. But please don't make posts like this.

At best, Endtest needs to conduct a more thorough competitive analysis. At worst, this is negligent product defamation, risks damaging people's livelihoods, and should be reported.

Cypress is free to use with any CI provider without requiring Cypress Dashboard. For OSS projects, Dashboard is free. Dashboard does assist with parallelization, but there are free open source mechanisms available to achieve this without a license as well.

Collapse
 
liviufromendtest profile image
Liviu Lupei

Hi Wes, I'm sorry you got that idea.

Please allow me to add a few more details to make things more clear.

Cypress is somewhat of a competitor for Selenium, Puppeteer, Playwright or TestCafe, but definitely not for Endtest.

Because the way they work is completely different.

Endtest is a cloud platform for people who don't want or do not have the time to write code for their automated tests.

Cypress is a npm package for people who want to write e2e tests with JavaScript.

I just mentioned some aspects that you left out of your article.

And I mentioned them because you're describing Cypress in a very positive way without mentioning the limitations and this might give readers an incorrect perception.

When describing a solution, we should always mention the limitations.

I don't see any defamation, the facts that I have mentioned are true and can be easily verified.

Accessibility also means running on all major browsers.

Collapse
 
coly010 profile image
Colum Ferry

You can run Cypress in CI without having to pay for a service I believe.

Collapse
 
liviufromendtest profile image
Liviu Lupei

I don't know. It seems so strange for an open-source solution to have a paid option which doesn't involve any cloud service.
The impression of those users was that the "open source" and "npm" nature of Cypress was just to attract developers without them knowing that there is a paid service that their company will need to buy if they decide to use it in an actual CI/CD system.
Like a "honeypot".
Again, not my words, not my opinion, only the feedback that I heard.

Collapse
 
jwp profile image
John Peters

Cypress is a industry disrupter. It crosses multiple testing definitions and can be used for almost any phase. Its http interception distinguishes it as the premier leader in the field. It's easy to learn and fun at the same time. Serious tooling for the serious test effort.

Collapse
 
malloc007 profile image
Ryota Murakami

So nice post!
Introducing 'pragmatic' frontend testing history from several years ago.
Jest coming, Kent made React Testing Library, starting service Sypress.io, each history written by clean text that contain at the time background.

I want to recommend someone who web developer! 🤗

Collapse
 
coly010 profile image
Colum Ferry

Awesome article, so many tips spread throughout this on top of the approach to using Cypress!

Some comments have been hidden by the post's author - find out more