Acceptance Testing React Apps with Jest and Nightmare

We love testing our JavaScript apps with Jest and Nightmare

Jest is a batteries included unit testing framework by Facebook. It's fast, feature rich, and integrates perfectly with Babel, an important tool our build pipeline. Jest allows for an exceptional unit testing experience.

However I've never been able to say that about acceptance testing. Could we integrate high-level, end-to-end tests and maintain the same experience?

Nightmare, an Electron powered high-level browser automation library, gets us really close. Using Nightmare, we can spin up a headless browser and perform user actions from any testing framework that leans on NodeJS.

Jest is one of those frameworks. In this blog post, I'll walk through how I set up Jest and Nightmare on my most recent project for great testing success.

Sample Boilerplate

This blog post builds from a setup I've created that leans on create-react-app. Feel free to check it out, or keep following along.

Using Jest

Before getting into Nightmare, let's look at a basic unit test:

import React from 'react'
import App from '../../src/app'
import {mount} from 'enzyme'

test('welcomes the user to React', function () {
  const wrapper = mount(<App />)

  expect(wrapper.text()).toContain('Welcome to React')
})

Here we're using Enzyme to mount our React and assert that it renders the correct text. For those unfamiliar with Enzyme, think of it like jQuery for testing React components. Only on steroids.

If we run the test, we'll see something like:

$ npm test

> jest test/unit

PASS  test/unit/app.test.js
  ✓ welcomes the user to React (29ms)

Test Summary
 › Ran all tests matching "test/unit".
 › 1 test passed (1 total in 1 test suite, run time 1.256s)

Neat. But this blog post is all about acceptance testing. Let's dig in.

Using Nightmare for acceptance testing

Nightmare makes it easy to quickly spin up a headless browser inside of a JavaScript testing framework. It leans on Electron, providing an environment very similar to Chrome.

First let's look at a test:

import nightmare from 'nightmare'

describe('When visiting the homepage', function () {

  test('it welcomes the user', async function () {
    let page = nightmare().goto('http://localhost:3000')

    let text = await page.evaluate(() => document.body.textContent)
                         .end()

    expect(text).toContain('Welcome to React')
  })

})

Here we spin up an instance of Nightmare and visit the homepage. When that finishes, grab text from the page and assert that it includes what we expect. Since Jest supports the async/await syntax, we can eliminate what would otherwise be a chain of callbacks.

Aside: One of our commenters has pointed out that Nightmare exposes many other behaviors such as clicking an element, changing the value of a form input, or trigging an event. Checkout the full documentation to learn more.

Back to the example! This is tidy, but tedious. What if the URL or port changes? Let's write a test helper that hides this all away:

import nightmare from 'nightmare'
import url from 'url'

const BASE_URL = url.format({
  protocol : process.env.PROTOCOL || 'http',
  hostname : process.env.HOST || 'localhost',
  port     : process.env.PORT || 3000
})

export default function (path='', query={}) {
  const location = url.resolve(BASE_URL, path)

  return nightmare().goto(location)
}

Awesome. Incorporating this in to our acceptance test:

import visit from '../helpers/visit'

describe('When visiting the homepage', function () {

  test('it welcomes the user', async function () {
    let page = visit('/')

    let text = await page.evaluate(() => document.body.textContent)
                         .end()

    expect(text).toContain('Welcome to React')
  })

})

All set! Now we can dynamically change the base URL for the host application without updating all of our tests.

What about Continuous Integration?

At Viget, we love CircleCI. It's fast and extremely configurable.

Let's set up a basic test script that hosts a production build and hammers it with our automated tests:

# This script is used by CircleCI to execute automated tests.

# Build all assets
npm run build

# Switch to the build directory
pushd build

# Boot a static file server
php -S localhost:3001 &

# Save the PID of the server to a variable
APP_TEST_PID=$(echo $!)

# Execute tests
PORT=3001 CI=true npm run test:all

# Exit the build directory
popd

# Kill the server
kill $APP_TEST_PID

Basically:

  • Run the production build
  • Host the build directory
  • Run tests against that server
  • Clean up.

The last step is to configure CircleCI:

machine:
  node:
    version: stable
test:
  override:
    # -e here configures the script to bomb out on an error
    - bash -e scripts/test.sh

That's it! Now whenever this project pushes to CircleCI, it will automatically run acceptance tests against a production build. This is super handy for automated deployments. Acceptance tests run against the build that will ship to our servers.

Wrapping up

We've come full circle. Jest gives us a fast and powerful testing environment. By taking advantage of Nightmare's ease of use, we can use the same testing framework to conduct both unit and acceptance testing.

Check out the sample project here, learn more about our React services here, and feel free to post comments about how you're testing your JavaScript apps!

Nate Hunzaker

Nate is a senior developer working from New York City, where he focuses on client-side application development. Most days, you can find him neck-deep in JavaScript working with clients such as The Nature Conservancy and the Wildlife Conservation Society.

More articles by Nate