Acceptance Testing React Apps with Jest and Nightmare

Nate Hunzaker, Former Development Director

Article Categories: #Code, #Front-end Engineering

Posted on

We love testing our JavaScript apps with Jest and Nightmare

Updates

Updated for CircleCI 2.0

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.
# For this to work, add pushstate-server to your project:
#   yarn add -D pushstate-server

# Build all assets
yarn build

# Switch to the build directory
yarn pushstate-server build &

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

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

# 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:

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:8-browsers
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
          name: "Restore Yarn Package Cache"
          keys:
            - yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
            - yarn-packages-{{ .Branch }}
            - yarn-packages-master
            - yarn-packages-
      - run:
          name: "Install Dependencies"
          command: yarn install
      - run:
          name: "Test"
          command: 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!

Related Articles