Ideas for Configuring, Organizing, and Consolidating Your Cypress Tests #frontend@twiliosendgrid

December 14, 2020
Written by
Opinions expressed by Twilio contributors are their own

Ideas for Configuring, Organizing, and Consolidating Your Cypress Tests #frontend@twiliosendgrid

We’ve written many Cypress tests across different web applications and frontend teams here at Twilio SendGrid. As our tests scaled to many features and pages, we stumbled onto some useful configuration options and developed ways to better maintain our growing number of files, selectors to our page’s elements, and timeout values along the way. 

We aim to show you these tips and ideas for configuring, organizing, and consolidating your own Cypress related things, so feel free to do with them what works best for you and your team.

This blog post assumes you have some working knowledge of Cypress tests and are looking for ideas on better maintaining and improving your Cypress tests. However, if you are curious to learn more about common functions, assertions, and patterns you may find useful to write Cypress tests against separate environments, you can check out this one thousand foot overview blog post instead. 

Otherwise, let’s carry on and first walk you through some configuration tips for Cypress.

Configuring your cypress.json

The cypress.json file is where you can set all the configuration for your Cypress tests such as base timeouts for your Cypress commands, environment variables, and other properties. 

Here are some tips for you to try out in your configuration:
  • Fine-tune your base command timeouts, so you don’t have to always add { timeout: timeoutInMs } throughout your Cypress commands. Tinker with the numbers until you find the right balance for settings such as “defaultCommandTimeout,” “requestTimeout,” and “responseTimeout.”
  • If your test involves iframes, you most likely need to set “chromeWebSecurity” to false so you can access cross-origin iframes in your application.
  • Try blocking third-party marketing, analytics, and logging scripts when you execute your Cypress tests to boost speed and not trigger unwanted events. You can easily set up a deny list with their “blacklistHosts” property (or “blockHosts” property in Cypress 5.0.0), taking in an array of string globs matching the third-party host paths.
  • Adjust the default viewport dimensions with “viewportWidth” and “viewportHeight” for when you open up the Cypress GUI to make things easier to see.
  • If there are memory concerns in Docker for weighty tests or you would like to help make things more efficient, you can try modifying “numTestsKeptInMemory” to a smaller number than the default and set “videoUploadOnPasses” to false to focus on uploading videos for failed test runs only.

On another note, after tweaking your Cypress configuration, you can also add TypeScript and better type your Cypress tests like how we did in this blog post. This is especially beneficial for auto-completion, type warnings, or errors when calling and chaining functions (like cy.task(‘someTaskPluginFunction), Cypress.env(‘someEnvVariable’), or cy.customCommand()).

You may also want to experiment with setting up a base cypress.json file and loading up a separate Cypress configuration file for each test environment, such as a staging.json when you run different Cypress scripts in your package.json. The Cypress documentation does a great job of walking you through this approach.

Organizing your Cypress folder

Cypress already sets up a top-level cypress folder with a guided structure when you first start up Cypress in your codebase. This includes other folders such as integration for your spec files, fixtures for JSON data files, plugins for your cy.task(...) functions and other configuration, and a support folder for your custom commands and types.

A good rule of thumb to follow when organizing within your Cypress folders, React components, or any code in general is to colocate things together that are likely to change together. In this case, since we’re dealing with web applications in the browser, one approach that scales well is to group things in a folder by page or overarching feature.

We created a separate pages folder partitioned by feature name such as “SenderAuthentication” to hold all the page objects underneath the /settings/sender_auth route such as “DomainAuthentication” (/settings/sender_auth/domains/**/*) and “LinkBranding” (/settings/sender_auth/links/**/*). In our plugins folder, we also did the same thing with organizing all the cy.task(...) plugin files for a certain feature or page in the same folder for Sender Authentication, and we followed the same approach for our integration folder for the spec files. We’ve scaled our tests to hundreds of spec files, page objects, plugins, and other files in one of our codebases—and it’s easy and convenient to navigate through.

Here is an outline of how we organized the cypress folder.
One other thing to consider when organizing your integration folder (where all your specs live) is potentially splitting up spec files based on the test’s priority. For example, you may have all the highest priority and value tests in a “P1” folder and the second priority tests in a “P2” folder so you can easily run all the “P1” tests by setting the --spec option like --spec ‘cypress/integration/P1/**/*’.

Create a spec folder hierarchy that works for you so you can easily group specs together not only by page or feature such as --spec ‘cypress/integration/SomePage/**/*’, but also by some other criteria like priority, product, or environment.

Consolidating element selectors

When developing our page’s React components, we typically add some level of unit and integration tests with Jest and Enzyme. Towards the end of the feature development, we add another layer of Cypress E2E tests to be sure everything works with the backend. Both the unit tests for our React components and the page objects for our Cypress E2E tests require selectors to the components/DOM elements on the page we wish to interact with and assert on.

When we update those pages and components, there is a chance for there to be drift and errors from having to synchronize multiple places from unit test selectors to the Cypress page object to the actual component code itself. If we relied solely on class names related to styles, it would be a pain to remember to update all the places that may break. Instead, we add “data-hook”, “data-testid”, or any other consistently named “data-*” attribute to specific components and elements on the page we wish to remember and write a selector for it in our test layers.

We can append “data-hook” attributes to many of our elements, but we still needed a way to group them together all in one place to update and reuse in other files. We figured out a typed way to manage all of these “data-hook” selectors to be spread out onto our React components and to be utilized in our unit tests and page object selectors for more reuse and easier maintenance in exported objects.

For each page’s top-level folder, we would create a hooks.ts file that manages and exports an object with a readable key name for the element and the actual string CSS selector for the element as the value. We will call these the “Read Selectors” as we need to read and use the CSS selector form for an element in calls such as Enzyme’s wrapper.find(“[data-hook=’selector’]”) in our unit tests or Cypress’s cy.get(“[data-hook=’selector’]”) in our page objects. These calls would then look cleaner like wrapper.find(Selectors.someElement) or cy.get(Selectors.someElement).

The following code snippet covers more of why and how we are using these read selectors in practice.

Similarly, we also export objects with a readable key name for the element and with an object like { “data-hook”: “selector” } as the value. We will call these the “Write Selectors” as we need to write or spread these objects onto a React component as props to successfully add the “data-hook” attribute to the underlying elements. The goal would be to do something like this and the actual DOM element underneath—assuming props are passed down to the JSX elements correctly—will also have the “data-hook=” attribute set.

The following code snippet covers more of why and how we are using these write selectors in practice.
We can create objects to consolidate our read selectors and write selectors to update fewer places, but what if we had to write many selectors for some of our more complex pages? This can be more error-prone to build out yourself, so let’s create functions to easily generate these read selectors and write selectors to eventually export for a certain page.

For the read selector generator function, we will loop through an input object’s properties and form the [data-hook=”selector”] CSS selector string for each element name. If a key’s corresponding value is null, we’ll assume the element name in the input object will be the same as the “data-hook” value such as { someElement: null } => { someElement: ‘[data-hook=”someElement”] }. Otherwise, if a key’s corresponding value is not null, we can choose to override that “data-hook” value such as { someElement: “newSelector” } => { someElement: ‘[data-hook=”newSelector”]’ }.
For the write selector generator function, we will loop through an input object’s properties and form the { “data-hook”: “selector” } objects for each element name. If a key’s corresponding value is null, we’ll assume the element name in the input object will be the same as the “data-hook” value such as { someElement: null } => { someElement: { “data-hook”: “someElement” } }. Otherwise, if a key’s corresponding value is not null, we can choose to override that “data-hook” value such as { someElement: “newSelector” } => { someElement: { “data-hook”: “newSelector” }’ }.
Using those two generator functions, we build out our read selector and write selector objects for a page and export them out to be reused in our unit tests and Cypress page objects. Another bonus is that these objects are typed in such a way where we cannot accidentally do Selectors.unknownElement or WriteSelectors.unknownElement in our TypeScript files as well. Before exporting our read selectors, we also allow adding extra element and CSS selector mappings for third-party components that we do not have control over. In some cases, we cannot add a “data-hook” attribute to certain elements, so we still need to select by other attributes, ids, and classes and add more properties to the read selector object as shown below.
This pattern helps us to stay organized with all of our selectors for a page and for when we need to update things. We recommend investigating ways for you to manage all these selectors in some sort of object or through any other means to minimize how many files you need to touch upon future changes.

Consolidating timeouts

One thing we noticed after writing many Cypress tests was that our selectors for certain elements would timeout and take longer than the default timeout values set in our cypress.json such as “defaultCommandTimeout” and “responseTimeout”. We would go back and adjust certain pages that needed longer timeouts, but then over time, the number of arbitrary timeout values grew and maintaining it became harder for large scale changes.

As a result, we consolidated our timeouts in an object starting above our “defaultCommandTimeout”, which is somewhere in the five to ten-second range to cover most of the general timeouts for our selectors such as cy.get(...) or cy.contains(...). Beyond that default timeout, we would scale up to “short”, “medium”, “long”, “xlong”, and “xxlong” within a timeout object we can import anywhere in our files to use in our Cypress commands such as cy.get(“someElement”, { timeout: timeouts.short }) or cy.task(‘pluginName’, {}, { timeout: timeouts.xlong }). After replacing our timeouts with these values from our imported object, we have one place to update to scale up or scale down the time it takes for certain timeouts.

An example of how you can easily start off with this is shown below.

Wrapping up

As your Cypress test suites grow, you may run into some of the same issues we did when figuring out how to best scale and maintain your Cypress tests. You can choose to organize your files according to page, feature, or some other grouping convention, so you always know where to look for existing test files and where to add new ones as more developers contribute to your codebase.

As the UI changes, you can use some sort of typed object (such as the read and write selectors) to maintain your “data-” attribute selectors to each page’s key elements you would like to assert on or interact within your unit tests and Cypress tests. If you find yourself starting to apply too many arbitrary values for things like timeout values for your Cypress commands, it may be time to set up an object filled with scaled values so you can update those values in one place.

As things change across your frontend UI, backend API, and Cypress tests, you should always be thinking about ways to more easily maintain these tests. Less places to update and make mistakes and less ambiguity as to where to put things make a huge difference as many developers add new pages, features, and (inevitably) Cypress tests down the road.

Interested in more posts on Cypress? Take a look at the following articles:

Recommended For You

Most Popular

Send With Confidence

Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.