UI testing with Jest and Puppeteer: an introduction

October 29, 2017 0 Comments

UI testing with Jest and Puppeteer: an introduction

 

 

I started to consider testing with Jest and Puppeteer right after the library came out. Puppeteer has quite an interesting API.

Testing with Jest and Puppeteer

In the following post I’ll introduce you to a basic UI test for a contact form.

We will testing with Jest and Puppeteer. Even if it’s still under development and the API could be subject to changes, Puppeteer is here to stay.

I was writing some tests last day and at the very same time I’ve come across a post by Kent C. Dodds.

Making your UI tests resilient to change” explains how to use  data-*attribute to make UI testing less fragile.

The data-*attributes are basically custom data attributes you can define on almost every HTML element. This is useful especially for exchanging data with Javascript.

Kent came at the right time because in all honesty I was doing something like:

await page.waitForSelector("#contact-form"); await page.click("#name"); await page.type("#name", user.name);

(you know, I’m more on the back-end side of things)

While I’m still not sold about using data-* for testing I must admit it is a good approach nonetheless. It’s beneficial for larger applications but for now I’ll stick with the classical way of selecting elements.

UI testing with Jest and Puppeteer: testing a contact form

My goal is to test a contact form for a landing page that I’m building.

Consider this:

UI testing with Jest and Puppeteer: testing a contact form

It has the following elements:

  1. a name input
  2. an email input
  3. a phone input
  4. a textarea
  5. a privacy checkbox
  6. a submit button

What do I want to test?

Testing the above form means asserting that a user can submit a contact request.

UI testing with Jest and Puppeteer: setting up the project

Let’s take a look at the tooling.

Testing with Jest and Puppeteer

Jest: a testing framework by Facebook. Jest provides a platform for automated testing along with a basic assertion library (Expect).

Puppeteer: a Node.js library for controlling headless Chrome. It’s rather new but it is a good time to check it out and see how it could fit inside your workflow.

Faker: a Node.js library for generating random data. Names, phones, addresses. Yeah, it’s kind of like Faker for PHP.

If you have already a project in place install the libraries with:

npm i jest puppeteer faker --save-dev

Installing Puppeteer will take some time because it ships with its own version of Chromium.

Chromium is the open source browser behind Chrome. Chromium and Chrome shares almost the same functionalities minus some license details.

Once the installation is done configure Jest in package.json. The testcommand should point to the Jest executable:

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

Also, in Jest I would like to write:

import puppeteer from "puppeteer";

To do so we need Babel for Jest. Let’s pull the dependencies in with:

npm i babel-core babel-jest babel-preset-es2015 --save-dev

and create a new file named .babelrcinside your project folder:

{ "presets": ["es2015"] }

With this in in place we can start to write a simple test.

UI testing with Jest and Puppeteer: writing the actual test

To start create a new directory inside your project folder: it could be testor spec. Then create a new file named form.spec.jsinside the same directory.

Now rather than throwing a lot of code at you I will break the test down starting from the import section. At the end we’ll see how the entire file looks like.

In order, import Faker and Puppeteer:

import faker from "faker"; import puppeteer from "puppeteer";

Configure the form url (you may want to test a development version on localhost rather than contacting the real website):

const APP = "https://www.change-this-to-your-website.com/contact-form.html";;

Create a fake user with Faker:

const lead = { name: faker.name.firstName(), email: faker.internet.email(), phone: faker.phone.phoneNumber(), message: faker.random.words() };

Define some variables for Puppeteer:

let page; let browser; const width = 1920; const height = 1080;

Define how Puppeteer should behave :

beforeAll(async () => { browser = await puppeteer.launch({ headless: false, slowMo: 80, args: [`--window-size=${width},${height}`] }); page = await browser.newPage(); await page.setViewport({ width, height }); }); afterAll(() => { browser.close(); });

Both beforeAll and afterAll are Jest methods. In brief, before running any test we must spawn a browser with Puppeteer. Then, a new page could be opened with browser.newPage().

When the test suite finishes running the browser must be closed with browser.close().

You’re not limited to beforeAll and afterAll, check out the Jest documentation to see what’s available. Anyway, it’s better to have one browser instance for the entire test suite rather than opening and closing a browser for every test.

A few things to note about the above code:

you can see I’m launching Chrome in its own window with headless: false. It’s because I needed to record a video to show you how the test works.

In real life you don’t want to see the actual browser. Simply remove all the options from within the launch() method.

Same for setViewport(), you can remove it. Or even better, you could set up two different environments: one for visual debugging and another for headless testing. Learn how.

Now we’re ready to define the actual test:

describe("Contact form", () => { test("lead can submit a contact request", async () => { await page.waitForSelector("[data-test=contact-form]"); await page.click("input[name=name]"); await page.type("input[name=name]", lead.name); await page.click("input[name=email]"); await page.type("input[name=email]", lead.email); await page.click("input[name=tel]"); await page.type("input[name=tel]", lead.phone); await page.click("textarea[name=message]"); await page.type("textarea[name=message]", lead.message); await page.click("input[type=checkbox]"); await page.click("button[type=submit]"); await page.waitForSelector(".modal"); }, 16000); });

Notice how it’s possible to use async/await within Jest. Assuming that you’re using one of the latest versions of Node.js.

Let’s break down the above test. This is what Chrome headless does when testing with Jest and Puppeteer:

  1. go to the page defined inside APP
  2. wait for the contact form to appear
  3. click and fill in every input
  4. check the checkbox
  5. submit the form
  6. wait for the modal to appear

Another note: notice the timeout (16000) passed to Jasmine as a second argument in test(). This is useful when you want to see Chrome interacting with the page.

When not in headless mode, if you do not configure a timeout you’ll get the following error:

Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL

Anyway, you could remove the timeout when Chrome runs in headless mode.

Now by running the test with:

npm test

I can see the magic happen:

NOTE TO SELF: The video has been recorded with recordmydesktop on Fedora, with the following options:

recordmydesktop --width 1024 --height 768 -x 450 -y 130 --no-sound

But wait!! There is more!

Testing with Jest and Puppeteer: testing the frontend and some more

Now that I’m happy with my contact form I could move on to testing some other elements inside the page.

Every web page should have a meaningful title right?

Let’s test that <title></title>is correct:

describe("Testing the frontend", () => { test("assert that <title> is correct", async () => { const title = await page.title(); expect(title).toBe( "Gestione Server Dedicati | Full Managed | Assistenza Sistemistica" ); }); // Insert more tests starting from here! });

What about a navigation bar? There should be one!

Testing that a navigation bar exists with Jest and Puppetter:

// test("assert that a div named navbar exists", async () => { const navbar = await page.$eval(".navbar", el => (el ? true : false)); expect(navbar).toBe(true); }); //

or testing that a given elements contains the expected text:

// test("assert that main title contains the correct text", async () => { const mainTitleText = await page.$eval("[data-test=main-title]", el => el.textContent); expect(mainTitleText).toEqual("GESTIONE SERVER, Full Managed"); }); // 

How do you feel about SEO? Check this out. Testing SEO concerns with Jest and Puppeteer, for example whether a canonical link exists or not:

describe("SEO", () => { test("canonical must be present", async () => { await page.goto(`${APP}`); const canonical = await page.$eval("link[rel=canonical]", el => el.href); expect(canonical).toEqual("https://www.servermanaged.it/";); }); });

and so on.

At the end of the day I’ll be mostly happy because of those green checkmarks:

Testing with Jest and Puppeteer: in testing we trust

Puppeteer gives you endless possibilities. A lot of folks are building new testing frameworks right now, with Puppeteer. The API could be improved, sure, but knowing the basics is a must.

And it plays nicely with Jest.

Testing with Jest and Puppeteer: where to go from here

You might not feel comfortable with Puppeteer itself or with the Puppeteer’s API. I feel you.

It’s rather new but it is a good time to check it out and see how it could fit inside your workflow.

Puppeteer is still in development and there will be improvements. In the meantime you can take a look at Cypress for example.

How do you test your applications? A lot of other folks are doing E2E testing with Puppeteer. And you?

Bonus: Testing with Jest and Puppeteer, visual debugging

With Puppeteer you can choose whether to launch Chromium in headless mode or not.

We’ve seen it before:

beforeAll(async () => { browser = await puppeteer.launch({ // Debug mode ! headless: false, slowMo: 80, args: [`--window-size=1920,1080`] }); page = await browser.newPage(); /// });

Also, you must provide Jasmine a timeout when launching the browser in visual mode. Otherwise the test will stop abruptly. The timeout is specified as a second argument for test():

describe("Contact form", () => { test( "lead can submit a contact request", async () => { ///// some assertions }, 16000 // <<< Jasmine timeout ); });

In an automated testing environment you don’t want to see the browser. It will take forever to run all the tests. So how to switch easily between visual debugging and headless mode?

Code yourself an helper function. Put it in some file named testingInit.js:

export const isDebugging = () => { let debugging_mode = { puppeteer: { headless: false, slowMo: 80, args: [`--window-size=1920,1080`] }, jasmine: 16000 }; return process.env.NODE_ENV === "debug" ? debugging_mode : false; };

Then you can reference the function inside your test:

/// import { isDebugging } from "./testingInit.js"; /// beforeAll(async () => { browser = await puppeteer.launch(isDebugging().puppeteer)); // <<< Visual mode page = await browser.newPage(); /// });

and

describe("Contact form", () => { test( "lead can submit a contact request", async () => { ///// some assertions }, isDebugging().jasmine // <<< Jasmine timeout ); });

Next time you’ll be able to use headless testing:

npm test

or the debugging mode:

NODE_ENV=debug npm test

Thanks for reading!

Photo credit: https://unsplash.com/@shotbyjames


Tag cloud