Make writing End to End tests child's play with Puppeteer and Jest

August 29, 2019Alex White6 min read

thumbnail

We all know writing tests is important. Unit testing is now a standard in software development, sometimes we even write them before the code. Snapshot testing is becoming more and more common to check that the UI doesn't change unexpectedly. However, the dev community still has a long way to go with testing user flows and making sure features actually work. That's why writing more End to End tests is important.

What is End to End testing?

End to end testing simulates user flows, essentially testing how a real user would use the application. This helps with development in two ways. Firstly it helps ensure that features work from a users perspective by running the tests. This means after you write new features, refactor existing code, or are about to release you can be confident that everything still works. Secondly, E2E tests act as documentation of all the user flows the application should handle and what features it provides.

So why don't we write enough E2E tests? E2E tests are often seen as painful to write, slow and flaky to run, and we have manual QA anyway right. Well, new tools and libraries have tackled these problems. Making writing and running E2E tests both fast and reliable. Frameworks like Cypress have tackled the first problem, coming with its own ide, having the ability to record tests, and being able to edit the tests live in a browser. Cypress makes writing end to end tests very easy, but they are still slow to run. Puppeteer is another framework that is still easy to write tests with but is also much faster at runtime.

What is Puppeteer?

So what is Puppeteer? In short, it is a node library to control a headless browser using the chrome devtools protocol. Puppeteer's Github and NPM are a great place to look if you're after more detail, but for now, this is all we need to know.

By default, Puppeteer downloads a matching version of headless chromium so we don't need to do any extra set up. To install Puppeteer on your machine all you need to do is create a new directory for your project and in that directory run yarn add puppeteer.

Now to show you how easy Puppeteer is to use, here is some example code where we will go to the Theodo website and take a screenshot of the homepage.

const puppeteer = require('puppeteer');

(async () => { const browser = await puppeteer.launch(); const page = browser.newPage(); await page.goTo('https://www.theodo.co.uk'); await page.screenshot({ path: 'theodo.png' }); await browser.close(); })();

The code is very self-documenting. To begin, a headless Chrome browser is launched, a new page is opened, the URL for that page is set to our website, a screenshot is taken and the browser is closed. The whole thing is readable, elegant, asynchronous and wrapped within an IIFE (immediately invoked function expression) that runs as soon as the script is run.

How does this help me with End to End tests?

So far I'm sure Puppeteer looks interesting and useful, but it probably seems better suited to automating tasks or scraping webpages. The missing piece is that we can combine Puppeteer with the testing libraries we already use. Leveraging the tools we already know and use day-to-day. Here at Theodo, the testing library we use most commonly is Jest. In this section, I'll show you how to quickly and easily set up using puppeteer with jest so you can start writing E2E tests today.

Lucky for us there is a package that provides most of the set up for using Puppeteer with Jest. All we need to do is install a few packages with yarn and modify some config files. Here are the steps if you also need to install Jest for the first time, but you can skip the second step if you are already using Jest on your projects.

  • Install the libraries yarn add --dev puppeteer jest jest-puppeteer
  • Generate the jest boilerplate jest --init
  • Update the jest config file generated to use jest-puppeteer { "preset": "jest-puppeteer" }

If you've been testing out the setup on your own machine you are now ready to start writing tests. To build on our example from before we are now going to go to the theodo homepage but this time check that the title is correct instead of taking a screenshot. Create a file called example.test.js and follow along with the example below.

describe('Theodo', () => { beforeAll(async () => { await page.goto('https://www.theodo.co.uk');
});

it('should be titled "Google"', async () => { await expect(page.title()).resolves.toMatch('Theodo UK'); }); });

Our first test is a simple follow on from our screenshot example from before. We go to the Theodo homepage, but this time instead of taking a screenshot we test that the page's title matches what we expect. You can see that it looks like a normal Jest test, we now just have access to Puppeteer within our tests with very minimal setup. This is obviously very trivial, so here is a repo of examples for you to use as a reference when you start writing more complex tests.

Configuring jest-puppeteer for easy writing and debugging

Puppeteer has a set of configurations that you can set to help with developing and debugging your tests. These are broken into a few categories, launch settings, connect settings (for when you're using an already running browser), browser settings (chromium or firefox), and browserContext settings. To use these a new file in the root of our project called `jest-puppeteer.config.js`. An example of what you might have inside is:

jsonmodule.exports = { launch: { headless: false, devtools: true, slowMo: 1200, }, browserContext: 'incognito', };

Here you can see the 4 config options you should be most familiar with.

  • headless: Whether to run the browser in headless mode.
  • devtools: Whether to auto-open a DevTools panel for each tab
  • slowmo: Slows down Puppeteer operations by the specified amount of milliseconds
  • browserContext: the browser context (cookies, localStorage, etc) shared between all tests

I recommend having headless false and devtools true whenever you are writing your tests as it will allow you to see what's happening as your script runs. If will writing you hit a problem and need to debug using slowmo allows you to have the script run at a pace that is easier to follow. This greatly improves our ability to see error and exceptions in the devtools and terminal as they happen. When actually running your test you will want to omit these setting as it is much faster and less resource-heavy.

The browserContext setting is useful if you want to have each test run in its own sandbox without being affected by the other tests. This is generally a good idea as it is easier to spot where problems come from when every test stands on its own, so I recommend using `browserContext: 'incognito'` as your default setting unless you have a good reason not too. Most E2E frameworks achieve this by starting new browser instances for every test, which works well but is very slow. Puppeteer gets the same benefits by using the incognito tabs already built into chromium but without the performance lose.

Bonus: Puppeteer vs. Cypress

To wrap up this article we are going to have a quick look at the cost/benefit of using Puppeteer over the current standard E2E testing framework, Cypress.

Cypress

Pros

  • Cross-browser support
  • Strong community
  • Good Docs
  • Built-in IDE
  • Can edit tests in the Browser
  • Can record tests
  • Assertions built-in

Cons

  • Doesn't support native browser events
  • Can only have one tab at a time
  • Slower
  • Paid

Puppeteer

Pros

  • Runs in a real browser
  • Open-source
  • Can be used for more than testing
  • Much faster tests
  • Actively developed by google
  • can have more than one tab/browser open

Con

  • Needs another library for assertions
  • Only has mature support for chromium and firefox
Alex White

Alex White

Web Developer at Theodo