Integration Testing with Cypress

Eugene Neymark
Life at Paperless Post
7 min readSep 11, 2019

Integration tests are used to validate that the user experience behaves as expected. Since we are testing a web app, we use a browser to perform these tests via Cypress.

Cypress is an end-to-end testing JavaScript framework and its architecture is uniquely suited for testing rich front-end applications. It provides an easy-to-use set of familiar tools for developers and QA.

At Paperless Post, Cypress integration tests use Chrome to interact with our app in the same way a user would in order to validate functionality and run through typical scenarios a user experiences.

Why Cypress?

Overall, Cypress is a well-supported framework built from scratch, versus other Selenium-based clones. It’s full-featured and comes with everything one needs to start automating their tests. We chose to work with Cypress because it waits automatically for everything to load before it interacts with the page and it comes with CLI (Command Line Interface) watcher support to re-run tests when code changes. Cypress is designed specifically for Chrome, and while Selenium supports other browsers as well, we specifically chose Cypress due to the speed of test suite execution. Cypress auto-records all the steps it takes to test a flow, which makes it easier to understand and fix failures. Developers can choose to mock responses and never need to access an underlying API for tests to be deterministic.

Need for integration tests

Testing is a tedious, repetitive, and necessary task. Some years ago, Paperless Post moved to service oriented architecture, and while individual services software has tests, we needed a way to test everything coming together.

Having integration tests can also help catch the most common user issues occurring from code changes during the Agile development process. While it is important to test using all methods, automating these common scenarios helps ensure our confidence in code changes without needing to rely on manual testing.

Testing functionality

For functional and unit tests, we consider code line coverage because those frameworks can easily generate a precise coverage percentage by looking at how many lines of code are called while tests are run. The industry standard for “good” is considered 80%; beyond that, we typically get diminishing returns for our efforts.

With Cypress, there is no straightforward way to calculate a percentage. Instead, we focus on the functionality that is most important to Paperless Post users: the “critical path” of inviting, RSVPing and managing follow up communications for events. We start by testing these most frequent workflows, followed by safeguards and error paths.

Writing Cypress tests

Let’s take that “critical path” I mentioned above and test the RSVP page. This test will ensure that the RSVP page:

  1. Loads successfully,
  2. Displays the correct event and venue information,
  3. Has an RSVP call to action that is visible and clickable, and
  4. Submits the response successfully.

These four basic things will tell us about the general health of the RSVP page. To do those actions, Cypress must have an understanding of the page’s HTML structure, or in Cypress’s terms, its “model”.

We can represent the RSVP with a model that abstracts the interactive components on the page. Defining the model makes it cleaner to add test declarations and maintain the code. When components on the page get moved around, we update only our model (our tests can be left alone).

To write our model, we need to mark important elements of the RSVP page with `data-test=` attributes. This indicates that the element is used in tests, making it simple for Cypress to find the element and alert developers.

In the Cypress page model, we access the element by

`cy.get(‘[data-test=“pages-desktop-form__done”]’)`

Any time we add an element to the page that we want Cypress to interact with, we add it to our model. This keeps the code clean and easy to follow.

Cypress uses Mocha’s BDD syntax. Use `describe()` to group tests that follow a similar path, allowing you to do some basic preliminary set up and define actions you want to run `beforeEach()` and `afterEach()` of the tests in that suite. `Aftereach()` can be used to clean up after the test (e.g., clear user cookies).

Cypress makes it easy to substitute any API call for some predetermined JSON. Using `cy.route.as` sets the same preconfigured response from the API every time. This is useful in situations where the API is unavailable and you want a truly deterministic test. Once you’re done setting up the routes, use `it()` to define what should happen in each scenario.

Interacting with a page using Cypress

Cypress uses our previously-defined page model to click a button that opens a form, type in some text, or make a selection from a pulldown menu. We use the `page` object which returns important HTML elements and performs interactions during the test.

Next, we ask Cypress to interact with one of our elements:

Since underlying elements of the page’s markup can change, keeping the model and steps of the test separate ensures that we only need to adjust the model when needed (and not the test). Elaborating on the screenshot above, we click on an element with `.click()` and then verify that the contents of the form are what we expect. Since we hardcoded the responses from our API, we know that the text sent is always “San Francisco” and we expect to find it in our validation step.

Running Cypress tests

There are several ways to install Cypress. Like any CLI-based tool, it can be pulled in manually from your project directory (`npm/yarn install cypress`) or added to your package.json and `npm/yarn install` will add it for you.

Once Cypress is installed and you have a test or two to run, `cd` to the directory that contains your test spec files and type `cypress open`. This opens the UI directly to the folder you are working in and you should see Cypress displaying your test specification files. Clicking one will run the test, so you’ll see Cypress open up a browser and execute the test. It’s easy to pass in different configurations under `Settings` for the start page so that you can test in various environments.

Running integration tests

At Paperless Post, we leverage continuous integration. Our process is to have each developer work on their own branch, in which a feature is built and locally tested. Then with each push to git, our automated tests run in CircleCI. Part of this process includes Cypress integration tests, which give us an early warning if anything is broken in our code.

Without getting into the details of our CircleCI implementation, any change triggers an execution of our Cypress framework and regresses the affected parts of our app using the test steps we defined. It will click, type, and look for elements that are there on the page (and it doesn’t even need a cup of coffee..!). Once it detects something is amiss, it takes a screenshot and alerts us of the failure and where it happened. Modular testing of React components has its place and is quite valuable, but it might not detect big-picture problems that Cypress detects immediately.

Most useful Cypress features

When developing in a front-end environment, Cypress is superior to other testing frameworks because it was built from the ground up using JavaScript. It comes with everything you need already set up, which means you can start writing tests instead of configuring. There are several features that make it especially useful:

  1. Mocking data from your API,
  2. Watching files for changes,
  3. Waiting automatically,
  4. Capturing screenshots, and
  5. Debugging with Devtools.

We talked about Cypress mocking the data from the server, allowing us to hardcode what we expect to see to verify that it is showing up in the correct place in the UI. This feature is extremely useful because some environments may not have access to your API and we don’t want tests to fail because the API is not reachable.

When developing locally, Cypress can watch for file changes and automatically rerun tests as you make changes, which makes it possible to validate your changes in real-time.

Unlike other frameworks, Cypress handles page actions and knows to wait for page loads or AJAX responses. This removes boiler plate code needed to do async operations and makes tests effortless, allowing developers to express their test steps easily.

Cypress captures every step of the test allowing us to go back and see exactly what our page looked like after each command. This is a huge asset in debugging a broken test.

Along with screenshots and videos, the tests are run in the Chrome browser and we can use devtools to troubleshoot. All the debugging techniques are available without the overhead. Just inspect or go into the debugger when you want to. Cypress makes it easy.

Final thoughts

Integration testing is worth the investment. If a feature is worth building, it’s worthwhile to write automated tests validating the feature. Cypress is a testing framework that is new but full featured and allows you to start testing quickly. There might be a bit of a learning curve depending on your previous experience but it is minimal in the areas of configuring and setting up. It was easily integrated into Paperless Post’s CircleCI process and alerts us when things break. The features Cypress provides are up-to-date and exactly up to the task of testing single page apps, like the one we’ve written in ReactJS.

--

--