Testing Express Apps with Mocha, Chai and Puppeteer

I've been working a lot on improving the test coverage for Klart.io recently. When the app was small I only had a few integration tests doing some API requests and asserting the response. The rest was tested manually in the browser. Now it's growing bigger both feature and user wise, so it made sense to automate more of the manual testing like end-to-end user signups through Stripe etc, since this took a lot of time.

For those that don't know about Klart, it's a bookmarking service for designers. This includes saving and serving loads of screenshots and links. Like most SaaS it also includes the usual landing page and signup flow.

Let's look at how I achieved end-to-end testing for a Node, Express and Mongoose app using Puppeteer (a head-less browser), Mocha and Chai (brilliant test libraries). Rather than just testing a basic HTML page, Let's look at the whole user signup flow to make things a bit more interesting.

This blogpost assumed that you have some experience with JavaScript, Node, Express and modern JavaScript like async/await.

Setting up Mocha and Chai

Let's start with setting up Mocha and Chai. Mocha is a really awesome testing library for JavaScript and Chai provides some nice ways to make assertions.

To set things up do:

npm install --save-dev mocha chai

Since I don't want to install these libraries when building for production, I'm using --save-dev. NPM will exlcude these when running npm install with NODE_ENV=production.

Let's also add a npm script to our package.json file to run the tests with npm test.

Finally, create a directory named test/ in your project root and Mocha will find it automagically when running npm test.

// package.json

{
  ...
  "scripts": {
    ...
    "test": "mocha"
  },
  ...
}

Running your app in test environment

To make your app available for testing you'll have to export your app object like this:

// server.js
const express = require('express');
const app = express();

// Some other code
// ....

// Export app object
module.exports = {
  app,
};

First test

Let's make a basic test to build upon. Create a new file in your /test directory called signup.spec.js.

// test/signup.spec.js
const chai = require('chai');
const { app } = require('../server');

// Tell Chai that we want to use the expect syntax
const expect = chai.expect;

// Some random port to use for testing
const PORT = 4000;

// Helper functions to start/stop app before/after tests
let server = null
const startApp = () => app.listen(PORT);
const tearDown = () => server.close();

describe('Signup', () => {
  before(startApp);
  after(tearDown);

  it('should signup user');
}

Good to go! We're ready to write our first test. Try things out by running npm test and you should see one test as pending for Signup.

Setting up Puppeteer

Puppeteer is a head-less browser that's super useful for things you would otherwise do in your browser manually. It's basically a scriptable browser window, except there's no window needed. Pretty cool.

To add Puppeteer to your project you install it with NPM. Again, you probably don't want to install Puppeteer on your production environment since it downloads a bundled version of Chromium as well (it can be relatively big).

npm install --save-dev puppeteer

Test signup flow

Let's add Puppeteer to our test and test the signup page (assumed to be located at /signup). There's a lot of code here but I've tried my best to explain each part in the comments.

// Include Puppeteer
const puppeteer = require('puppeteer');

// Other code...
// ...

// We're using async/await to keep things sane
it('should signup user', async () => {
  // Launch browser, create a new page and visit signup
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(`http://localhost:${PORT}/signup`);

  // Select input with id 'name-input' and type 'Fredrik' into it
  const nameInput = await page.$('#name-input');
  await nameInput.type('Fredrik');

  // Do the same with 'email-input' and 'password-input'
  const emailInput = await page.$('#email-input');
  await emailInput.type('[email protected]');
  const passwordInput = await page.$('#password-input');
  await passwordInput.type('foobar');

  // Sometimes Stripe can take a while to load
  // So we'll wait for a bit.
  await page.waitFor(1000);

  // Select our 'card-element' created using Stripe Elements API
  const cardInput = await page.$('#card-element');

  // Click it to start focusing
  await cardInput.click();

  // We cannot directly type into these inputs
  // But we can send normal keyboard events.
  // Let's do that with a delay of 100 ms to simulate actual typing.
  // We're using a Stripe testing card and taking advantage of
  // the fact that Stripe autofocuses on next input while typing.
  await page.keyboard.type('4242424242424242121812388888', { delay: 100 });

  // Finaly, submit.
  const submitBtn = await page.$('#submit-btn');
  await submitBtn.click();

  // Wait for Stripe to validate card and our backend to process it.
  // You might need to ad some time here, if you're on a slow connection.
  await page.waitFor(5000);

  // Expect to be redirected to dashboard
  expect(page.url()).to.include('/dashboard');

  // Finally close
  await browser.close();
});

That's it! Now we can test our user signup flow by just running `npm test` instead of typing credit card numbers like a crazy person. I also have tests for failed payments, form validation etc. I'll leave that for you as an exercise to implement :).

That's it!

That's it! Now we can test our user signup flow by just running npm test instead of typing credit card numbers like a crazy person. That's pretty awesome. I also have tests for failed payments, form validation etc. I'll leave that for you as an exercise :).

And oh, if you're looking for a sane way to organize bookmarks and screenshots. Try Klart!