How Cypress will make you love frontend testing

We’ve coded a lot of client side Javascript at Varvet lately, and decided to try Cypress for our frontend testing. Like Selenium, Cypress is used to automate the browser like a user, and is agnostic of your stack. Unlike Selenium Cypress is a complete, batteries-included, testing framework for web interfaces that helps you write, run and debug your tests.

Cypress is developed based on Lean Startup concepts, the team behind it started out by surveying developers about their key pain points when testing, and the result is a test framework that is easy to setup, has familiar syntax and has built-in errors for debugging. Cypress has been under development for a few years and went fully open source in early 2017. What’s so good about Cypress is that it performs like a browser but isn’t a browser.

Setting up Cypress

Cypress is built on top of Mocha, Chai and Sinon. You don’t need to add any extra dependencies. To get starting using Yarn as your package manager:

$ yarn add cypress --dev

$ open cypress

The first time you run Cypress, it will detect that it’s a new project and seed directories and configuration files.

When running Cypress with Ruby on Rails we purge the database between runs, using the following command:

$ bin/rake db:purge && cypress open

You can tell Cypress to prefix urls with your website’s URL configuring a base url in cypress.json.

{
  "baseUrl": "http://localhost:4567"
}

Writing tests

Cypress provides an API for writing tests. It’s probably familiar since it’s based on Chai and replicates jQuery’s functions for querying the DOM. Every command hangs off one global object, cy. A test for some of the navigation on varvet.com could look like this:

describe("main navigation", () => {
  it("can navigate", () => {
    cy.visit("/");
    cy.get("[data-cy='nav-blog']").click();
    cy.location("pathname").should("eq", "/blog");

    cy.viewport("iphone-6");
    cy.get("[data-cy='nav-blog']").click();
    cy.location("pathname").should("eq", "/blog");
    cy.get("[data-cy='nav-about']").click();
    cy.location("pathname").should("eq", "/about");
  });
});

You’re not expected to use assertions for everything, instead query for elements, using cy.get() for any selector or cy.contains() for content, and tests will break if elements don’t appear as expected. Cypress waits for a set time for elements to appear before failing.

Running and debugging tests

Cypress comes with a visual tool built on top of Electron. It performs nearly identical to Chrome (and doesn’t support other browsers). The visual tool is where you’ll spend your time viewing and debugging features. Cypress allows you to step through your test and pin snapshots for any command in the test that you would like to inspect, and gives you added information about keypresses and HTTP requests.

Did I mention that Cypress performs like a a browser? This means that you can use the developer console, log things and add debugger statements just as you would normally.

Running the above test in Cypress looks like this:

Cypress running tests on varvet.com

Below is an example of what Cypress looks like when inspecting a pinned snapshot. In this case the snapshot is for a POST request so Cypress gives some info about the request and response.

Cypress snapshot

Unit test your features by mocking

Cypress has been especially helpful when I’ve needed to test a view with different state. By using Sinon under the hood, Cypress provides functions for mocking.

To mock an API response:

it("can signup", () => {
  cy.server()
  cy.route({
  method: "POST",
  url: "http://localhost:3001/api/accounts/login/",
  status: 200,
  })
})

Cypress recommends stubbing and mocking your backend in most of your tests, and having a few E2E-tests for testing the real stack.

My latest project uses the browser’s built in timer a lot to start animations. Cypress exposes Sinon’s mock timers, which you can use for controlling and advancing time.

it("can start timer", () => {
  cy.clock()
  cy.click("Start timer")
  cy.tick(10000)
  cy.contains("Timer finished")
})

Be careful though, native timers aren’t restored until the end of the test.

Let Cypress act as your browser

We’ve previously relied mostly on Mocha, Chai, and headless specs with Capybara for frontend testing. What I expected from Cypress was easier setup, faster tests and being able to see what was going on in the browser.

It turns out I use it as a part-time stand-in for the browser when developing. As such has made testing a natural part of my frontend development workflow. I write a Cypress test whenever I find myself performing manual testing, it saves me clicking and typing, and lets me debug faster.