It’s Never Been Better to Get Started with Cypress Web Tests

Brian Griggs ·

If your project could at all be described as a web application, your UX pipeline would likely benefit from adopting Cypress. Beating out Selenium on speed as well as breadth of testing tools, Cypress provides automated testing of your website’s critical features in a matter of minutes.

A testing framework built on top of many familiar techs — Mocha, JQuery, Chai — Cypress can be a frustrating product because, though it looks familiar, it behaves in unfamiliar ways. But its powerful tooling means mastering this framework is well worth the effort.

Here are the most important things to know as you dive into Cypress.

To Mock or Not to Mock

Using cy.route, Cypress can intercept XHR requests, allowing you to spy on requests so as to assert your frontend presenters and backend contracts get along. It can even stub responses, which adds speed and determinism to your tests.

This is the most impactful feature Cypress has: it allows you the choice between a fully holistic end-to-end test or a fast integration test to prove out complex interactions. Test-writers should know when to employ each strategy, and the Cypress team has written extensively on how to thread this needle.

While Cypress builds with an integrations folder, I recommend making an additional endToEnd folder. Put un-stubbed tests in endToEnd, and exercise only your most critical pathways, as this test suite will assuredly be your slowest. Put tests that stub responses in integrations, and be sure to keep fixtures up-to-date with API changes — leveraging Cypress’s TypeScript support is a huge help in this effort.

What’s Good for the Spec is Good for the User

Flake, or sporadic false-negatives, is often the biggest complaint thrown at Cypress. This is fair and unfair criticism. The framework offers many tools to guard your test against flake. The simplest method is to observe XHR requests and deterministically decide, against the observer, when to continue with the test.

cy.server();
cy.route("POST", "/customer-service").as("customerService");
cy.visit("/help");
cy.fillOutCustomerServiceForm();
cy.contains("submit").click();
cy.wait("@customerService");
cy.contains("Submitted!").should("exist");

Many testers will rightfully argue that watching XHRs has no place in an automated user test: users are not aware of XHR — only the UI that expresses the results — so we should only assert against the UI. However XHR waiting is exactly how Cypress recommends reducing flake. How do we get around this?

A more expressive UI is the answer.

If your test can’t continue until an XHR completes, that means you have a UI-blocking XHR. That needs to be expressed to your user somehow, and then your tests can wait on: loading indicators; reenabling of the UI; status popups; etc.

cy.server();
cy.route("POST", "/customer-service").as("postToCustomerService");
cy.visit("/help");
cy.fillOutCustomerServiceForm();
cy.contains("Submitting Your Message").should("exist");
cy.contains("Submitting Your Message").should("not.exist");
cy.contains("Submitted!").should("exist")

Remember that Cypress is your user. If Cypress can’t notice things without cheats, your users won’t notice them either.

Don’t Nest Assertions

Testing async behavior has always been a little difficult to do. Cypress’s API does provide a then method. To most developers, it will look like a Promise’s then method — which is not a wrong assumption, but it’s also not totally correct.

cy.then does allow you to, like Promise.then, await async function returns for inspection. This would normally mean the rest of your spec would need to promise-chain off of it. But Cypress runs its commands independently off of a queue, held in a singleton. This means cy.then is merely promise-like, but not an actual promise.

// don’t do this!! It’ll work but be very flaky
cy.wait("@someXHR").then(({response}) => {
  expect(response).toEqual(fixture);
}).then(() => {
  cy.get("someOtherElement").should("have.css", { color: "FF0000" });
  cy.get("someOtherElement").click();
}).then(() => { … })

All Cypress commands are assertions. If a cy.get fails to find what it’s looking for, the test fails. Think of a cy.then as a child assertion: both parent and children are read by Cypress as one package. Cypress won’t move on to the next parent command until the previous parent, and all of its children, have passed.

// do this instead!! It looks wrong but it’s not
cy.wait("@someXHR").then({response} => {
  expect(response).toEqual(fixture);
});
cy.get("someOtherElement").should("have.css", { color: "FF0000" });
cy.get("someOtherElement").click();

How does Cypress achieve this? Like a good SAT test taker, it reads all assertions before it begins running them. Very deep ancestries are an easy way to confuse Cypress’s queue system: it might think it can move on to the next parent query too soon, and accidentally run commands out of their appropriate order.

The worst part about this issue is you likely won’t experience it early: at some point, the ancestry tree becomes a little too deep, and then the whole thing caves in. Worse yet, this usually begins to happen on CI before it happens locally, further hiding the real problem. Be declarative, and trust in Cypress’s queueing system.

User-Centric Selections & Aliases

Cypress leverages jQuery’s powerful DOM querying API, which allows you to select elements just about any way you’d like. But the most straightforward selection strategy can also be the most brittle.

Remember that the user is the supposed operator in your test, and users don’t notice class names — they may not even notice types — but they do notice presentation. Have your selectors run on things like text with cy.contains("Order Now!") or cy.get("[placeholder=username]"). Just be sure the text you render is deterministic.

This creates get commands that are hard to understand. The normal inclination would be to bind these gets to a nicely-named variable but, because Cypress is a singleton, we can’t do that. We can’t store the results of cy.get ever as a local variable. But we can register the selector with the test-runner via aliases:

cy.contains("Order Now!").as("orderNowButton");
cy.get("@orderNowButton").click();
cy.contains("Submitting Your Order").should("exist");
cy.get("@orderNowButton").should("be.disabled");

Expect to heavily leverage CSS attribute selectors, which makes sense because styling is what users notice. Aliases are key to creating strong Cypress tests, so be sure to learn them well.

Eager Assertions

Cypress is both patient and eager. It’s patient, as in any cy command will wait up to five seconds by default to be executable before the test fails.

However, it’s eager, as in where Cypress can perform a command, it will. Cypress then steps into the next command, which can easily lead you into some pitfalls. For instance, loops don’t retry assertions. Like you would with an it-block, try to keep every cy command as self-contained as possible.

function itemLooksGood($listItem) {
  // if this fails once, your whole test fails — no retries available
  cy.wrap($listItem).should("have.text", "looks good")
}

cy.wait("@forListToFullyLoad") // don’t skip this step!!!!!
cy.get("ui>li").each(rowLooksGood)

Test as You Dev

Automated user tests obviously provide your QA team with the biggest benefits … or do they? Every web developer has manually tested their website, clicking around through the same flow over and over to make sure what you’re building looks good — but it doesn’t have to be that way!

Manually set your connections in Cypress via env variables to always point at your test database. That way you can leave Cypress open as you dev and let the tests drive your development, from the outside in — that is, from assertions that mirror your acceptance criteria to the nitty-gritty of the implementation.

Do It All in Three Different Browsers!

Feature testing with Cypress just became much more appealing: earlier this year, support for Mozilla Firefox and Microsoft Edge shipped with version 4. With this setup of fast, flake-free and deterministic tests that can be run during development, programmers can hope to be alerted to Edge or Firefox issues while they are devving. This feedback loop from bug to discovery is tighter than many teams had thought they could ask for.

Cypress 3 could only run tests in Google Chrome. Support for Edge and Firefox marks a shift for Cypress towards a critical arena of web design: browser compliance. Likely there will be some disappointment as developers have to wait longer for support of arguably the hardest browsers to dev for: Internet Explorer and Safari. But the fact that Cypress now supports two browser engines (Chromium and Gecko) should give developers hope for future versions with expanded browser support.

At Carbon Five, we’ve used Cypress on many projects to tighten the bug discovery feedback loop. If you try out Cypress on your own project, I hope these best practices come in handy, and save you time in ramping up with this powerful tool.

Illustrations by Erin Murphy.


Interested in more software development tips & insights? Visit the development section on our blog!