End-to-End Testing with Cypress

#
By Peter Tasker

This week on the blog we’re going to talk about something I’m a big fan of: testing.

Don’t fall asleep juuuust yet.

If you’re like me, during development, you probably refresh your browser and click on buttons a million times a day to make sure you didn’t break anything. This clicking-on-buttons-and-checking-it-still-works is essentially what fancy people call end-to-end testing. I’ve been doing this a lot recently while testing the Theme & Plugin Files Addon and working through a refactor of the WP Migrate DB Pro codebase.

Fortunately, like other types of testing, there is a way to automate end-to-end testing.

In this post we’re going to cover Cypress and how it simplifies end-to-end testing in the browser. You can use Cypress to test any ‘critical path’ on your site. This can be actions like making sure your sales funnel still works after a code push, or that a contact form submits correctly. Anything you would normally test by hand you can automate with Cypress!

Why Cypress?

Cypress is a new-ish test runner that aims to simplify end-to-end testing.

Before Cypress you’d have to figure out which testing library to use (Mocha, Karma, Jest), install Selenium, choose an assertion library, choose a mocking library, lose your mind and then write your tests. With Cypress the steps are: install Cypress -> write tests.

Cypress has all the testing goodies already rolled in so you can literally just jump in and start writing tests.

Sound to good to be true? It’s not!

Getting Started

Like it says on the Cypress website, cd into your project and run:

npm install cypress

Once it’s done it will create a ./cypress folder in your project. In there, a few sub-folders will be created. The most important for writing tests is the /integration folder as this is where we write tests.

Cypress comes pre-loaded with a bunch of example tests, but it’s not too difficult to create your own from scratch.

In the ./cypress/integration folder any JavaScript file you create will be picked up by the Cypress runner.

We’ll create a new JavaScript file here so we can write our first test. The Cypress docs give a good overview of this, but I thought it would be better to see how to test a WordPress project or plugin.

Writing Tests

The first thing we need to do is set up a ‘describe’ block. This basically tells Cypress that this is a test and gives the test a name.

describe( 'Run a pull', function() {
    //...
}

Like Mocha, Cypress provides the ‘beforeEach’ function. This is a really helpful place to put things you want to run before each test is run. For WP Migrate DB Pro, I wanted to login to WordPress and go to the WP Migrate DB Pro page in wp-admin before my tests were run:

const baseURL = Cypress.env( "site" ).url;

beforeEach( function() {
    cy.visit( baseURL + '/wp-login.php' );
    cy.wait( 1000 );
    cy.get( '#user_login' ).type( Cypress.env( "wp_user" ) );
    cy.get( '#user_pass' ).type( Cypress.env( "wp_pass" ) );
    cy.get( '#wp-submit' ).click();
} );

Side-note: Logging in through the UI is a Cypress ‘Anti-pattern’ but it does work.

The Cypress.env(...) statements are pulling data from a cypress.json file that’s placed in the root of the project. This is one of several ways to store environment variables with Cypress.

My cypress.json looks like this:

{
    "env": {
        "wp_user": "admin",
        "wp_pass": "secret",
        "site1" : {
            "url" : "http://cypresstests.devtest",
            "path" : "~/Sites/cypresstests.devtest",
            "remote_connection" : "https://anothercypresstest.devtest\n7bpxn3S6qihNEjgA4o6qlzj1MvcMUOkt9zHoEzqC"
        },
        "site2" : {
            "url" : "http://testingsite.local",
            "path" : "~/Local\\ Sites/testingsite/app",
            "remote_connection" : "https://othertestingsite.local\n68hQT30jPr9d9x/IH+XATrW8q2e7ZkKZ+d19BqlO"
        },
        "plugins_to_migrate" : [
            "iThemes Sync",
            "Google Analytics for WordPress by MonsterInsights",
            "Hello Dolly"
        ],
        "themes_to_migrate" : [
            "Twenty Seventeen (active)"
        ]
    }
}

You’ll also notice in the beforeEach() function we’re using a cy object. Cypress exposes this object to interact with the Cypress runner and have access to all the test functions. The above code is pretty straightforward, we use cy.visit() to load up a URL, wait 1 second for the page to load, and then type in the login credentials.

cy.get() is similar to jQuery selectors or document.querySelector() and we use it to get a reference to a DOM element. From there we can manipulate the element by using cy.type() or cy.click(). A full list of all the interaction methods can be found in the Cypress’ API documentation.

Inside the ‘describe’ block you can use the Mocha style it() statement to describe the different parts of your test.

it( 'can run a pull', function() {
    //..
}

In my case, I don’t really care if my test can do all the other things WordPress can do (login, go to the WP Migrate DB Pro page, etc.), all I want to test is if it can run a pull.

For the test itself, we’ll add some code in the it() block:

it( 'can run a pull', function() {
    cy.visit( baseURL + '/wp-admin/tools.php?page=wp-migrate-db-pro&wpmdb-profile=-1' );
    cy.get( '#pull' ).click();
    cy.get( '.pull-push-connection-info' ).type( Cypress.env( "site2" ).remote_connection );
    cy.get( '.connect-button' ).click();

    cy.wait( 2000 );

    //...More test code
    // Click the migrate button
    cy.get( '.migrate-db-button' ).click();
} );

In the above test we’re going to a new URL (/wp-admin/tools.php?page=wp-migrate-db-pro&wpmdb-profile=-1) to load up the WP Migrate DB Pro new profile page and we’re clicking on the #pull selector, which selects the ‘pull’ migration type.

We then tell Cypress to type in some connection information, hit connect, and wait two seconds. After that we hit the ‘Pull’ button to start the migration.

Our full test should now look like this:

describe( 'Run a pull', function() {
    const baseURL = Cypress.env( "site" ).url;

    beforeEach( function() {
        cy.visit( baseURL + '/wp-login.php' );
        cy.wait( 1000 );
        cy.get( '#user_login' ).type( Cypress.env( "wp_user" ) );
        cy.get( '#user_pass' ).type( Cypress.env( "wp_pass" ) );
        cy.get( '#wp-submit' ).click();
    } );

    it( 'can run a pull', function() {
        cy.visit( baseURL + '/wp-admin/tools.php?page=wp-migrate-db-pro&wpmdb-profile=-1' );
        cy.get( '#pull' ).click();
        cy.get( '.pull-push-connection-info' ).type( Cypress.env( "site2" ).remote_connection );
        cy.get( '.connect-button' ).click();

        cy.wait( 2000 );

        //...More test code
        // Click the migrate button
        cy.get( '.migrate-db-button' ).click();
    } );
} );

The great part with Cypress is that you don’t need to assert anything. Cypress will fail if a JavaScript error occurs or an element doesn’t exist. You can assert that elements exist and do comparisons against an expected state, but it’s not required.

Running tests

So far we’ve been neck-deep in code, and we haven’t seen any of the magic that Cypress provides. Let’s see what happens when we actually run this test.

Back in our terminal, at our project root, we’ll run npx cypress open. The Cypress app will open and you’ll see something like this:

Cypress UI

What!

That’s right folks, with Cypress you get a UI with your testing framework!

Mind blown 🤯.

In the Cypress app you can click on individual tests to run them or click ‘Run all specs’.

In my case I named the pull test mdb_pull_spec.js so we’ll click on that one and let it run.

Cool right? The sidebar on the left shows each of the steps Cypress is taking as outlined in the test. The great thing about Cypress is that it takes screenshots at each step of the test so you can go back and see what the state of the UI was at that point.

You can also click on each snapshot to see the UI state at that point in the test. For XHR (ajax) requests, you can see the UI state before and after the request was made.

Cypress before and after XHR

CLI

As if that wasn’t enough, Cypress also has a built in CLI runner for your tests. Don’t want to click a button or even see the tests run? Just run npx cypress run in your project root:

Running Cypress on the command line also records a video(!) of the test run so you can debug more easily if/when a test fails.

With the CLI runner you can hook Cypress into your CI workflow and run it each time you push your code.

Caveats

This all looks pretty great but what sucks about Cypress, Pete?!

Well, for one, Cypress doesn’t handle actual browser interaction well. I noticed this while trying to write a test for the WP Migrate DB export functionality. When the export file is generated the browser prompts for a download.

Cypress doesn’t (yet) have a way to interact with these browser dialogs, so that’s something to keep in mind if your app uses a file selector or depends on browser interaction.

One test runner to rule them all

I really have to tip my hat to the developers of Cypress, it’s a really great test runner and for me, makes it incredibly simple to add end-to-end tests to a legacy application.

I’ve also only briefly covered what Cypress can do, there are a ton of other features I didn’t go over like CI integration, stubbing out network requests and variables and handling async code!

I hope that gives you a quick overview of what Cypress has to offer and how you can integrate it into your WordPress development workflow.

What do you think about end-to-end testing and Cypress? How do you handle testing critical paths? Let us know in the comments!

About the Author

Peter Tasker

Peter is a PHP and JavaScript developer from Ottawa, Ontario, Canada. In a previous life he worked for marketing and public relations agencies. Love's WordPress, dislikes FTP.