⚡lightning-fast testing of web applications with Cypress
Cypress (Cypress.io) is an automation framework for web app testing built and configured with Javascript. Automated front-end testing is definitely not new, but Cypress really is something different. It’s silly fast, requires almost no setup, has quick-to-learn syntax and has a really nice, feature packed test runner.
Why Cypress? I’ll let you read the summary on the summary page at cypress.io, whilst also stealing this image from their blurb
TL;DR: Why have all those libraries to manage, drivers to install and syntax’s to remember?!
Don’t we already have tons of testing frameworks?
Yes. I have previously used tooling like Selenium with C#, and know our QA team use paid tooling like Sahi Pro, for a start.
Whilst these tools are OK, they often feel clunky with tooling oddities and not-to-friendly syntax. Supplementary to this, a lot of these tools are Selenium based which means they are all sharing the same annoyances.
Setting up
To get going with Cypress, simply run the NPM command: npm install cypress --save-dev
within the folder you want to use Cypress from. Note that Yarn variants are also available and can be found on their site.
If the command executes successfully, you should have a new ./node_modules
directory and the package-lock.json.
To setup and open Cypress for the first time, simply execute the command below, whilst in the context of your installation folder.
./node_modules/.bin/cypress open
This will do a couple of things:
- Create a folder named
cypress
within your working directory – This will be where all your test descriptions and configuration lives - Opens up the Cypress App.
Feel free to explore the examples which provide samples of common tests, but we won’t cover them in this post.
Project structure
If you open the Cypress folder in VS code, you will find the default project files for a Cypress project.
Integration: This folder will contain all the spec files for this project. Creating sub folders within here will be echoed in the test runner. For example, you may have a folder structure like ./integration/cms/account which contains just the tests for the account functionality. How you structure this is up to you.
Support: The support folder contains 2 files, index.js
and commands.js
. The index.js
file will be ran before every single test fixture and is useful if you need to do something common like reset state. The index file also imports the commands.js
file.
commands.js
is imported by the index file, and is another place to store common code, but has the advantage that it can be called from any test fixture, at any stage. An example of this could be storing the login method here under a command named DoLogin
which saves having to define this in every fixture.
Plugins: Contains a single file index.js
which is a jump-off point for importing or defining changes to how Cypress works.
Diving into testing with a real example
Creating and running tests
First of all, I will delete the examples folder. For this post I will be “testing” the Twitter desktop site as all my real examples are for enterprise or private software.
NOTE: This software is not designed for general browsing automation and should only be used against websites you maintain/own. In fact, a lot of sites try to block this and I actually struggled to find a public site I could use this against consistently!
Create a test fixture/spec
Create a new file underneath the “Integration folder” named “MyTest.spec.js” the “.spec” is a naming standard for defining specifications which I suggest you keep, but isn’t strict.
The structure of this file should be as follows:
describe("Twitter example tests", function() {
it("Page should load", function() {
cy.visit("https://twitter.com/login");
});
});
Each file contains a single description, which in turn can contain many steps. I advise a high level of granularity when writing tests, such as a test spec for testing the login page with several steps is fine, having one that tests your website with hundreds of steps, not so much.
If you save this file, and still have the test runner open, it should have automatically found this new test. If you closed the runner, simply re-run the ./node_modules/.bin/cypress open
command again.
Clicking on this test will open up a new browser instance (based on the one selected in the drop down – seen in the top right of the screenshot above). The test runner will open a split-window with the executing tests (and results) on the left, and the browser view on the right.
Of course, this test passes as it doesn’t actually *do* anything! Let’s change this! You don’t need to close this runner either, as any changes to this test will be picked up automatically and re-ran.
Basic Interactions
For this example, we will take the existing test above and have it test logging in to the website and navigating to the settings panel.
Loading a web page: A redirect or page load is done with cy.visit(url)
. For this example we used cy.visit("https://twitter.com/login");
Locating an element: This is done similar to how jQuery finds objects in that you can find them on type, id, class or data attribute. The flow is always to find an item first, then chose what to do with it. For this we need to find 2 text boxes- one for user and one for password.
As Twitter does some magic with their element classes I will be locating the boxes by their unique attributes. If I use the code below, you can see the test will pass as it finds the element on the page. Hovering over the test in the test steps will highlight the matching field.
describe("Twitter example tests", function() {
it("Page should load", function() {
cy.visit("https://twitter.com/login");
cy.get("input[name='session[username_or_email]']");
});
});
Interacting with an element: Once we have located the element we can interact with it with methods such as .type()
, .click()
and more. In this example I want to set the username and password field appropriately and then click the enter button, so the code now looks like:
describe("Twitter example tests", function() {
it("Page should load", function() {
cy.visit("https://twitter.com/login");
cy.get("input[name='session[username_or_email]']")
.first()
.type("MyHandle");
cy.get("input[name='session[password]']")
.first()
.type("password1234");
cy.get("form[action='/sessions']")
.first()
.submit();
});
});
If we run this now we can see that the page is loaded, the form is filled out and the form is submitted. The test passes, but should fail as the actual login fails due to incorrect details.
Finding text: One way we could validate if the test above succeeds is to check for the existence of an object, or some text on the page which states the login was not a success. To do this we can add the line cy.contains("The username and password you entered did not match our records. Please double-check and try again.");
which will check the entire DOM for that specific text. We could also find a specific element using .get()
and chaining on the .contains()
method.
Waiting: Waiting is part of all web applications, and although Cypress will retry a few times if it cannot locate an element, it does not have a long timeout. The cy.get()
takes in an additional options object in which a timeout can be specified. For example: cy.get(".some-class-which-isnt-visible-yet", { timeout: 30000 });
would pause the execution of the test until the element is located, or the 30,000ms timeout occurs.
Code sharing and re-use
Lets say we have expanded our tests so we have a new test which detects if the word “Home” is displayed to the user on their dashboard once logged in.
describe("Twitter tweet tests", function() {
it("When logged in the word Home appears", function() {
cy.contains("Home");
});
});
Running this will fail as it doesn’t know which website to use. We could use the cy.visit()
method, but as each test is ran is isolation of the others we wouldn’t be logged in. Whilst we could just copy the login code from the first test into this one (either in the it
method, or in a beforeEach
block), its a little messy to do so and introduces duplication and more maintenance.
Commands & Shared Code
Remember that commands.js file under the Support directory? Lets create a new command which will do our login from a central place! We will simply cut and paste in the contents of the login section of the previous test, like so:
Cypress.Commands.add("twitterLogin", () => {
cy.visit("https://twitter.com/login");
cy.get("input[name='session[username_or_email]']")
.first()
.type("MyValidUser");
cy.get("input[name='session[password]']")
.first()
.type("MyPassword");
cy.get("form[action='/sessions']")
.first()
.submit();
});
This tells Cypress that there is a command available called “twitterLogin” and which steps to execute when this command is called. Now we can simply update the login.spec.js to be:
describe("Twitter tweet tests!", function() {
it("Can compose a tweet", function() {
cy.twitterLogin();
cy.contains(
"The username and password you entered did not match our records. Please double-check and try again."
);
});
});
Now, we can call cy.twitterLogin()
from any of our spec files!
Final thoughts
Cypress may well become my favorite UI testing framework. In less than a day I was able to gain enough knowledge to put together a fairly large proof of concept for testing one of our front end applications. The only “difficulties” were things like persisting authentication which only took a few google searches to solve. I may have other posts around adding additional flexibility in the future.
The main benefit to me (other than the flexibility, speed, and the obvious) is that the syntax is flexible enough for a developer, but easy enough for somebody with less coding knowledge (QA, BA, etc).