I love end to end testing. I think it’s one of the best ways to make sure your entire web application is healthy. Unit tests don’t tell you that a feature is working, and even running integration tests for your microservices doesn’t tell you that all your systems let your users log in.

So let’s write some end to end tests to do just that. I’m going to use Mocha with Chai as the test runner and assertion library, and I’ll use puppeteer to manipulate a Chrome headless browser.

Setting Up the Project

Let’s create a new project and install our dependencies:

yarn init

yarn install --dev puppeteer mocha chai

Mocha is our test runner. Chai is our assertion library. Puppeteer is the library we will be using to interact with a Chrome browser. When you install puppeteer, it will automatically install the latest version of Chrome so that you are guaranteed to have headless mode.

Once we have our dependencies installed, we’re going to need to add a few files so that we can use the browser and do some clean up after our tests.

First let’s create a simple wrapper for the browser that puppeteer gives us using a Proxy.

browser.js

const puppeteer = require('puppeteer'); /**

* This is a thin wrapper so that we use a singleton of

* the browser that puppeteer creates

*/

class Browser {

setUp(done) {

const puppeteerOpts = this.options && this.options.puppeteer ?

this.options.puppeteer :

{}; puppeteer.launch(puppeteerOpts).then(async b => {

this.setBrowser(b);

done();

});

} setBrowser(b) {

this.browser = b;

const oldNewPage = this.browser.newPage.bind(this.browser); this.browser.newPage = async function () {

const page = await oldNewPage();

this.lastPage = page; return page;

};

} setOptions(opts) {

this.options = opts;

} test(promise) {

return (done) => {

promise(this.browser, this.options)

.then(() => done()).catch(done);

};

}

} /*

* Create a new browser and use a proxy to pass

* any puppeteer calls to the inner browser

*/

module.exports = new Proxy(new Browser(), {

get: function(target, name) {

return name in target ? target[name].bind(target) : target.browser[name];

}

});

This let’s us use a singleton to store options that we will pass in and the browser object from puppeteer and provide some helper functions for using it.

Next let’s add some before and after functions for mocha so that we can boot the browser up and clean up after ourselves.

runner.js

const path = require('path');

const slug = require('slug')

const browser = require('./browser');

const options = require('./options'); before((done) => {

browser.setOptions(options);

browser.setUp(done);

}); after(() => {

browser.close();

});

And if you want to pass in variables to your tests, or options to puppeteer, create a options.js:

Writing a Test

We have our scaffolding set up, write a test. We’re going to import the test helper function that we wrote in our singleton so that we can easily pass down the browser and options to our test.

tests/auth/canLogin.js

const { expect } = require('chai');

const { test } = require('../../browser'); describe('Login', () => {

it('can login', test(async (browser, opts) => {

// ...

}));

});

Let’s try to login to a form and test that it passes:

const page = await browser.newPage();

await page.goto(`${opts.appUrl}/login`);

await page.click("[name=email]");

await page.type("test@test.com")

await page.click("[name=password]");

await page.type("testing"); await page.click("[type=submit]"); const DASHBOARD_SELECTOR = 'div > ul > li > button > span';

await page.waitFor(DASHBOARD_SELECTOR); const innerText = await page.evaluate((sel) => {

return document.querySelector(sel).innerText;

}, DASHBOARD_SELECTOR); expect(innerText).to.be.equal('Dashboard');

Finishing Up

All that’s left is running our test!

mocha --timeout 10000 ./runner.js tests/*/**.js

You’ll see the normal output from mocha!

Be sure to follow me if you want more articles on doing end to end testing. I’ll be going over how to get puppeteer set up with docker and how to get screenshots for when tests fail!