Cypress is a powerful integration testing framework for the browser that promises to aleviate a lot of the common paint points of testing with Selenium. Projects have been trying to unseat Selenium for years, but Cypress differentiates itself with its exceptional UI and developer experience, something that Selenium has always struggled with. Cypress has powerful features like automated screenshots of test failures, time travel, and stubbing built-in. Right now the big limitation of Cypress is that they only support testing in Chrome and Electron. In this article, I'll demonstrate how to test a simple autocomplete app with Cypress.

A Vanilla JS Autocomplete

In order to demo writing a test with Cypress, I put together a simple dependency-free autocomplete that hits npm's public search API to search for npm packages. Below is a video showing the autocomplete in action. This app won't be on Dribbble anytime soon but it is enough for a meaningful demo of Cypress' features.

Below is the HTML for the autocomplete page, the index.html file:

<html>
  <body>
    <div id="content">
      <div id="search-wrapper">
        <input type="text" id="search" placeholder="Press enter to search npm">
      </div>
      <div id="content-wrapper">
        <ul id="results"></ul>
      </div>
    </div>
    <script type="text/javascript" src="/index.js"></script>
  </body>
</html>

The HTML isn't particularly interesting. The real functionality is in the index.js file. This file is responsible for instrumenting the HTML to send out an HTTP request to npm using fetch() and rendering the results as an unordered list. To avoid promise chaining, the below example uses async/await.

init(document.querySelector('#search'), document.querySelector('#results'));

const endpoint = 'https://registry.npmjs.org/-/v1/search';

function init(input, results) {
  let inProgress = false;
  input.addEventListener('keydown', async (ev) => {
    if (ev.key !== 'Enter' || inProgress) {
      return;
    }

    const text = input.value;
    let result = null;
    inProgress = true;
    try {
      result = await window.fetch(`${endpoint}?text=${text}&size=25`).
        then(res => res.json());
    } catch (error) {
      inProgress = false;
      throw error;
    }
    inProgress = false;

    render(result.objects.map(v => v.package));
  });

  function render(packages) {
    results.innerHTML = packages.
      map(pkg => `
        <li>
          <a href="http://www.npmjs.com/package/${pkg.name}">
            ${pkg.name}
          </a>
        </li>
      `).
      join('\n');
  }
}

One neat feature of this example is that it doesn't require any bundlers like webpack. Most real apps will use webpack to compile the apps they test with Cypress, but this example just uses a <script> tag in the interest of staying focused and concise.

Testing the Autocomplete with Cypress

Installing Cypress is easy via npm:

npm install cypress

In order to easily run Cypress, add cypress:open script to your package.json file.

{
  "name": "cypress-autocomplete",
  "devDependencies": {
    "cypress": "3.0.2",
    "serve": "9.2.0"
  },
  "scripts": {
    "cypress:open": "cypress open",
    "serve": "serve ./"
  }
}

In one terminal tab, run npm run serve to start an HTTP server that serves files from your file system. In another tab, run npm run cypress:open to start Cypress. When you run Cypress, you should see a window like what you see below.

Next, add a test file sample.js to the cypress/integration folder. Below is a simple test for the autocomplete that stubs out the npm API and asserts that the autocomplete renders some fake data.

describe('Autocomplete', function() {
  it('renders results', function() {
    cy.visit('http://localhost:5000', {
      onBeforeLoad: win => {
        // Stub out `window.fetch()` so `index.js` doesn't actually hit
        // the npm API
        cy.stub(win, 'fetch', () => {
          return Promise.resolve({
            json: () => ({
              objects: [
                { package: { name: 'mongoose' } },
                { package: { name: 'mongoose-autopopulate' } }
              ]
            })
          });
        });
      }
    });

    // `{enter}` is how you simulate hitting the 'enter' key in Cypress
    // See: https://docs.cypress.io/api/commands/type.html#Arguments
    cy.get('#search').type('mongoose{enter}');

    // Don't use `expect()` directly here, chaining calls onto `cy`
    // is potentially async
    cy.get('#results').
      should('contain', 'mongoose').
      and('contain', 'mongoose-autopopulate');
  });
});

Find this test file in the Cypress test runner and click it. Cypress will then run the test for you. Below is a video of the process end-to-end.

Moving On

Cypress has a lot of excellent features that this article didn't cover, like time travel and screenshots. Although the limited set of browsers is a problem, Cypress' features make it a compelling option for integration tests. Next time you're thinking about testing tools for your frontend, make sure you check out Cypress.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus