Testing your JavaScript in a browser with Jest, Puppeteer, Express, and Webpack

James Anthony Bruno
ITNEXT
Published in
6 min readJun 13, 2018

--

UPDATE (2019/04/17): Reformatted the test server code to cover more general cases. Also included information about setting up jest-puppeteer.config.js.

In a couple of recent projects, I found myself having to test code which could only be run in the browser. There are many ways for one to do so, but I found out the most documentation with jest-puppeteer, which is based on Jest, Facebook’s JavaScript testing framework, and Puppeteer, the official headless Google Chrome API. This article will demonstrate how to get started with jest-puppeteer and how to easily use it with Webpack.

You can see the full example source code on GitHub.

Example project

After months of deliberation and research, you’ve come up with the hot new Node.js library: foo. foo is an innovative new function that returns the string bar when called. Astonishing!

Below is the starting directory structure for foo:

foo/
| index.js
| package.json

index.js

function foo() {
return 'bar';
}
module.exports = foo;

Step 1: Installation

In your Node.js project, you’re going to need to install modules like so:

yarn add -D express jest jest-cli jest-puppeteer puppeteer webpack webpack-dev-middleware

Or,

npm i -D express jest jest-cli jest-puppeteer puppeteer webpack webpack-dev-middleware

What each modules does

  • jest and jest-cli: This is what we’re going to use to test the logic of our program.
  • puppeteer: This will allow us to headlessly interact with a webpage as if we’re a user. It can also inject scripts to run into the webpage which can come in handy depending on the type of program we’re making.
  • jest-puppeteer: The marriage of Jest and Puppeteer. Adds automatic setup and teardown of Puppeteer and optionally local servers for testing browser code.
  • express: Express will be what we use as a local testing server.
  • webpack and webpack-dev-middleware: We can use these modules to automatically serve browser-compiled JavaScript to our test server webpages.

Step 2: Webpack setup

Next, you should setup Webpack for your project if you haven’t already. Nothing special needs to happen here—just configure it how you need it.

webpack.config.js

module.exports = {
entry: './index.js',
output: {
filename: 'index.min.js',
library: 'foo'
},
mode: 'production'
}

Step 3: Server setup

Next, you’re going to want to setup your test server. First, make a directory called test inside your project. In this folder, create a JavaScript file called server.js.

Let’s build this file step-by-step (you can see the complete file on GitHub).

const express = require('express')
const webpack = require('webpack')
const middleware = require('webpack-dev-middleware')

We’ll need Express as our web server and Webpack and webpack-dev-middleware for injecting our compiled scripts into the page.

const compiler = webpack(require('../webpack.config.js'))

Here, we’re initializing a Webpack compiler based on the webpack.config.js file. We could also cut out the middleman and just have the settings defined right here if needed. This would allow us to have separate settings for the test environment.

// Turns input into an array if not one already
function normalizeArray (arr) {
return Array.isArray(arr) ? arr : [arr]
}
// Gets all the Javascript paths that Webpack has compiled, across chunks
function getAllJsPaths (webpackJson) {
const { assetsByChunkName } = webpackJson
return Object.values(assetsByChunkName).reduce((paths, assets) => {
for (let asset of normalizeArray(assets)) {
if (asset != null && asset.endsWith('.js')) {
paths.push(asset)
}
}
return paths
}, [])
}
// Optionally, just get the Javascript paths from specific chunks
function getJsPathsFromChunks (webpackJson, chunkNames) {
const { assetsByChunkName } = webpackJson
chunkNames = normalizeArray(chunkNames)
return chunkNames.reduce((paths, name) => {
if (assetsByChunkName[name] != null) {
for (let asset of normalizeArray(assetsByChunkName[name])) {
if (asset != null && asset.endsWith('.js')) {
paths.push(asset)
}
}
}
return paths
}, [])
}

In this section, I’m defining some helpful functions for handling the output of webpack-dev-middleware. getAllJsPaths will return an array of paths to all JavaScript files generated by the middleware. getJsPathsFromChunks will only return the JavaScripts paths that belong to the chunk(s) defined in the second argument. Feel free to choose one or adjust with your own criteria!

let port = 4444
const index = Math.max(process.argv.indexOf('--port'), process.argv.indexOf('-p'))
if (index !== -1) {
port = +process.argv[index + 1] || port
}
const app = express()
.use(middleware(compiler, { serverSideRender: true }))
.use((req, res) => {
const webpackJson = res.locals.webpackStats.toJson()
const paths = getAllJsPaths(webpackJson)
res.send(
`<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<div id="root"></div>
${paths.map((path) => `<script src="${path}"></script>`).join('')}
</body>
</html>`
)
})
.listen(port, () => {
console.log(`Server started at http://localhost:${port}/`)
})

There’s a couple things happening here. In the first bit, we’re determining the port number in case it was passed in with either --port or -p. By default, the port will be 4444.

Additionally, we define the Express server and attach webpack-dev-middleware to it. We then handle its output using the getAllJsPaths function to generate our test webpage with JavaScript in tow. Finally, the server is then told to listen on the port number which had been previously defined.

Feel free to adjust the HTML to fit the needs of your project. You could even get fancy and use templating engines such as Pug to generate your HTML. For more comprehensive testing, you can use routing to create different pages for different tests.

Now, you should go into your package.json file and add a script that starts your server like so:

{
...
"scripts": {
"serve": "node test/server.js"
},
...
}

This can be ran by yarn serve (or npm run serve). You can then access the page at http://localhost:4444.

Step 4: Jest setup

Almost there! Now we need to get Jest and jest-puppeteer sorted out.

Either through your package.json or through jest.config.js, configure Jest.

package.json example

{
...
"jest": {
"preset": "jest-puppeteer",
"globals": {
"PATH": "http://localhost:4444"
},
"testMatch": [
"**/test/**/*.test.js"
]
},
...
}

jest.config.js example

module.exports = {
preset: "jest-puppeteer",
globals: {
PATH: "http://localhost:4444"
},
testMatch: [
"**/test/**/*.test.js"
]
}

Your specific configuration may need to look different from this, but at the least it should have "jest-puppeteer" set as the preset. Here, I also add the server address as a global variable and changed the test match pattern so that it wouldn’t match test/server.js.

You will also need to configure jest-puppeteer by defining a jest-puppeteer.config.js file (official docs):

module.exports = {
server: {
command: 'npm run serve',
port: 4444
}
}

You should also add Jest as a script in your package.json:

{
...
"scripts": {
"serve": "node test/server.js",
"test": "jest"
},
...
}

Step 5: Create your tests

Guess what! We’re pretty much done. Now you just need to write your tests! Refer to the Jest, Puppeteer, and jest-puppeteer documentation to construct your test cases. Below is an example test case for our amazing foo module.

test/foo.test.js

describe('foo', () => {
beforeEach(async () => {
await page.goto(PATH, { waitUntil: 'load' })
})
test('should return bar', async () => {
const foo = await page.evaluate(() => {
console.log('foo');
return foo();
})
expect(foo).toBe('bar')
})
})

This test case starts by navigating to localhost:4444 (our PATH) and waiting until load time. This will navigate before each test in order to freshly reinitialize the page. Swapping beforeEach with beforeAll will have the test framework only navigate once, with all subsequent tests run on the same page instance.

The single test case in this instance asynchronously runs a script within the page’s context to work the magic that is foo. It then uses Jest’s matcher framework to make sure the results are to be expected.

Step 6: Run your tests

All you need to do is run yarn test (or npm test) in the command line!

Further reading

This is just a starting point. I suggest reading up on the various modules used so you can customize them to fit your project.

Also, if you want to see this in practice with a real module, check out the source code for Alchemize. It is from this project that I derived the example repository and this step-by-step documentation.

Alternatives

The only notable alternative that has 100% coverage of the above process that I can find is Karmatic. Karmatic is a zero-configuration test framework that wraps Karma, Webpack, Jasmine, & Puppeteer into one module.

You also can potentially swap out some of the key modules in this (Jest, Puppeteer, Express, Webpack, etc.) with something comparable as long as you keep the concepts and process the same.

Feel free to share in the comments any alternatives that you are using that you feel work really well!

--

--