SETTING UP A FRONTEND TESTING
FRAMEWORK USING JEST AND PUPPETEER

ATLARGE
ATLARGE

Our team is passionate about delivering the best. Achieving this means a lot of emphasis is put on Quality Assurance, an iterative process that is repeated until our product reaches our standard of quality. Frontend user interface testing is one of the final and continuous iterations of this process and I will share with you a bit of how we accomplish this below.

Selenium has been the go-to for front end testing for the past decade or so, and has built a reputation as a “flaky” tool, where tests often fail at random. This is very stressful, frustrating, and wastes a lot of your time trying to debug. One day your tests are passing with flying colors and the next you’re getting all red. 

Selenium is so flaky people typically advise for short test runs with multiple tests because the longer a single test is ran the more chance there is for it to flake out. This tool also has huge dependencies (i.e Java Runtime Environment) and versioning can get messy very quickly if you add in any extra packages or plugins. I have found using Puppeteer to drive the browser and Jest as my test runner to be more reliable, lightweight, and more enjoyable to work with overall.

“Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.”

This means we do not have to rely on a selenium server and web driver connection, Puppeteer has complete control over the Chromium browser through it’s DevTools protocol. 

In this post, I will show you how to create a basic Jest + Puppeteer testing suite. We will use this to run some automated tests on the Google homepage to demonstrate the web driver and assertions.

TO BEGIN.

Let's Get Started

Create a new directory, call it whatever you would like. I will be calling mine ‘google-test’. 

$ mkdir google-test && cd google-test
$ yarn init -y

I will be using Yarn for this tutorial, you may use NPM and achieve similar results but they are not guaranteed. If you would like more info on how to install and use Yarn visit https://yarnpkg.com

Install the packages and dependencies we need for this: 

$ yarn add chalk jest puppeteer rimraf --save

A breakdown of the packages we just installed:

Jest: A very fast and robust test runner by Facebook. https://github.com/facebook/jest
Chalk: A tool for styling terminal output. https://github.com/chalk/chalk
Puppeteer: A headless Chrome Node API developed by the Google Chrome dev team. https://github.com/GoogleChrome/puppeteer
Rimraf: The UNIX command rm -rf for node. https://github.com/isaacs/rimraf

DIRECTORY ARCHITECTURE.

Time to Organize

Now that we have our packages let’s organize the directory architecture.

$ touch setup.js teardown.js puppeteer_environment.js jest.config.js
$ mkdir __tests__

Here is an assortment of configuration files and a directory where we will store our tests. Every javascript file in here will be ran during Jest execution. I will provide example configurations to use for this tutorial and a brief explanation of what these files do. 

This file wires up Jest with Puppeteer, informing Puppeteer where to look for test setup and teardown instructions, and environment functions.
jest.config.js

module.exports = {
  globalSetup: './setup.js',
  globalTeardown: './teardown.js',
  testEnvironment: './puppeteer_environment.js'
}

 

This file sets up environmental variables and functions for Puppeteer.
puppeteer_environment.js

module.exports = {
  globalSetup: './setup.js',
  globalTeardown: './teardown.js',
  testEnvironment: './puppeteer_environment.js'
}

This file sets up environmental variables and functions for Puppeteer.
> puppeteer_environment.js


const chalk = require('chalk')
const NodeEnvironment = require('jest-environment-node')
const puppeteer = require('puppeteer')
const fs = require('fs')
const os = require('os')
const path = require('path')

const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup')

class PuppeteerEnvironment extends NodeEnvironment {
  constructor(config) {
    super(config)
  }

  async setup() {
    console.log(chalk.yellow('Setup Test Environment.'))
    await super.setup()
    const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf8')
    if (!wsEndpoint) {
      throw new Error('wsEndpoint not found')
    }
    this.global.__BROWSER__ = await puppeteer.connect({
      browserWSEndpoint: wsEndpoint,
    })
  }

  async teardown() {
    console.log(chalk.yellow('Teardown Test Environment.'))
    await super.teardown()
  }

  runScript(script) {
    return super.runScript(script)
  }
}

module.exports = PuppeteerEnvironment

 

This file provides a function for setting up and providing parameter values for Puppeteer’s configuration. A notable observation to see in here is that we designate ‘headless’ to be ‘false’ so we are able to see Chromium run through our test.
setup.js

const chalk = require('chalk')
const puppeteer = require('puppeteer')
const fs = require('fs')
const mkdirp = require('mkdirp')
const os = require('os')
const path = require('path')

const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup')

module.exports = async function() {
  console.log(chalk.green('Setup Puppeteer'))
  const browser = await puppeteer.launch({ headless: false, slowMo: 500, waitUntil: 'networkidle' })
  // This global is not available inside tests but only in global teardown
  global.__BROWSER_GLOBAL__ = browser
  // Instead, we expose the connection details via file system to be used in tests
  mkdirp.sync(DIR)
  fs.writeFileSync(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint())
}

 

This file contains code that is ran once the tests are complete, this simply logs a closing message and closes the browser.
teardown.js

const chalk = require('chalk')
const rimraf = require('rimraf')
const os = require('os')
const path = require('path')

const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup')

module.exports = async function() {
  console.log(chalk.green('Teardown Puppeteer'))
  await global.__BROWSER_GLOBAL__.close()
  rimraf.sync(DIR)
}

 

Inside the package.json file, you will want to add the following scripts section:

"scripts": {
    "test": "jest"
  },

 

You can place this block right above dependencies. (You should expect it to end up looking like https://github.com/billmakes/jest-puppeteer/blob/master/package.json)

Now that we have our configuration files in place we are ready to begin writing the Google homepage frontend test. 

WRITING THE TEST.

Inside the __tests__ directory, create a new test file. I will name mine test.js for this tutorial. 

$ touch __tests__/test.js

This file contains the test, instructions for the web driver, and assertions. Copy and paste this code in your test.js file.
test.js

const timeout = 10000
// typingSpeed value is set for wait time between keystrokes. Simulates realistic typing.
const typingSpeed = 50

describe(
  'Google search test',
  () => {
    let page
    beforeAll(async () => {
      jest.setTimeout(timeout)
      page = await global.__BROWSER__.newPage()
      await page.goto('https://google.com')
    }, timeout)

    afterEach(async () => {
      await page.waitFor(1000)
    })

    afterAll(async () => {
      await page.close()
    })

    it('Google homepage loads', async () => {
      await page.waitForSelector('input[type="text"]')
      await page.waitForSelector('input[type="submit"]')
      await page.type('input[type="text"]', 'Stack Overflow', {delay: typingSpeed})
      await page.click('input[type="submit"]')
    })

    it('Navigate to the first result', async () => {
      await page.click('#rso > div:nth-child(1) > div > div > div > div > h3 > a')
      await page.waitFor(1000)
    })

  },
  timeout
)

 

We have configured and have put a test in place. Let’s go ahead and run it! In the terminal type:

$ yarn test

You will see the test execute along side of a Chrome instance running through the steps listed in the test. 

WHAT'S NEXT?

Coming up

I know there is much more left to be desired but that will need to wait for another tutorial. Today we set up Jest and Puppeteer and put together a very basic test on the Google homepage. I highly recommend reading the Puppeteer API, there is a ton of commands and assertions you can wield to create very robust and extensive test suites for your projects.

https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md

For more resilient tests, I highly recommend reading this blog post from Kent C. Dodds https://blog.kentcdodds.com/making-your-ui-tests-resilient-to-change-d3…

Utilizing data attributes you are able to get rock solid selectors on key elements used for testing applications. While creating these data attributes is also a great time to document them, making test writing and maintenance later on a breeze. I use this technique personally and my tests never flake!

All of the code used in this tutorial can be found here: 
https://github.com/billmakes/jest-puppeteer

LIKE WHAT YOU JUST READ?

Contact Us