Dealing with Asynchrony when Writing End-To-End Tests with Puppeteer + Jest

Albert Alises
QMENTA Tech Blog
Published in
10 min readOct 15, 2018

In this article we present an overview on how to deal with asynchrony when performing end-to-end tests, using Puppeteer as a web scraper and Jest as an assertion library.

We will learn how to automate user action on the browser, wait for the server to return data and for our application to process and render it, to actually retrieving information from the website and comparing it to the data to see if our application actually works as expected for a given user action.

This article appeared originally on Dev.to 🤖

You finally got your wonderful web application up and running, and the time for testing has come…. There are many types of test, from Unit tests where you test the individual components that compound your application, to Integration tests where you test how these components interact with eachother. In this article we are gonna talk about yet another type of tests, the End-To-End (e2e) tests.

End-to-end tests are great in order to test the whole application from a user perspective. That means testing that the outcome, behavior or data presented from the application is as expected for a given user interaction with it. They test from the front-end to the back-end, treating the application as a whole and simulating real case scenarios. Here it is a nice article talking about what e2e tests are and their importance.

To test javascript code, one of the most common frameworks for assertions is Jest, which allows you to perform all kinds of comparisons for your functions and code, and even testing React components. In particular, to perform e2e tests, a fairly recent tool, Puppeteer, comes to the rescue. Basically it is a web scraper based on Chromium. According to the repo:

“Puppeter is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol”

It provides some methods so you can simulate the user interaction on a browser via code, such as clicking elements, navigating through pages or typing on a keyboard. It is like having a highly trained monkey perform real case tasks on your application 🐒

You can find the github repos of both testing libraries here:

Given so, the Puppeteer + Jest become has become a nice, open source way of testing web applications, by opening the application on the headless browser provided by puppeteer and simulating user input and/or interaction, and then checking that the data presented and the way our application reacts to the different actions is as expected with Jest.

In this article we will not cover the whole workflow of testing with puppeteer + jest (such as installing, or setting the main structure of your tests, or testing forms), but focus on one of the biggest things that we have to take into account when doing so: Asynchrony.

🤘 But hey, here you have a great tutorial on how to test with Puppeteer + Jest, to get you started.

Almost all of the web applications contain indeed some sort of asynchrony. A request is sent to the back-end. Then we wait for the result of that request and retrieve the data from the back-end response, and that data is then rendered on screen. However, Puppeteer performs all the operations sequentially, so… how can we tell him to wait until asynchronous events have happened?

Figure 1: I feel you, but sometimes you have to wait just a little bit for everything to be rendered and ready, so your e2e tests do not crash and burn

Puppeteer offers you a way to wait for certain things to happen, using the waitFor functions available for the Page class. The changes you can track are visual changes that the user can observe. For instance you can see when something in your page has appeared, or has changed color, or has disappeared, as a result of some asynchronous call. Then one would compare these changes to be what you would expect from the interaction, and there you have it. How does it work?

Waiting, Waiting, Waiting…⏰

The waitFor function set in Puppeteer helps us deal with asynchrony. As these functions return Promises, usually the tests are performed making use of the async/await ES2017 feature. These functions are:

1) waitForNavigation

await page.waitForNavigation({waitUntil: "networkidle2"});

Whenever a user action causes the page to navigate to another route, we sometimes have to wait a little bit before all the content is loaded. For that we have the waitForNavigation function. It accepts an options object in which you can set a timeout (in ms), or waitUntil some condition is met. The possible values of waitUntil are (according to the wonderful puppeteer documentation):

  • load (default): consider navigation to be finished when the load event is fired.
  • domcontentloaded: consider navigation to be finished when the DOMContentLoaded event is fired.
  • networkidle0: consider navigation to be finished when there are no more than 0 network connections for at least 500 ms.
  • networkidle2: consider navigation to be finished when there are no more than 2 network connections for at least 500 ms.

Typically, you will want to wait until the whole page is loaded ({waitUntil: load}), but that does not guarantee that everything is operative. What you can do is wait for a specific DOM element to appear that assures you that the whole page is loaded. You can do that with the following function:

2) waitForSelector

This function waits for a specific CSS selector to appear, indicating that the element it matches to is on the DOM.

3) waitForFunction

The last one waits for some function to return true in order to proceed. It is commonly used to monitor some property of a selector. It is used when the selector is not appearing on the DOM but some property of it changes (so you cannot wait for the selector because it is already there in the first place). It accepts a string or a closure as arguments.

For example, you want to wait until a certain message changes. The testing would be performed by first getting the message using the evaluate() Puppeteer function. Its first parameter is a function which is evaluated in the browser context (as if you were typing into the Chrome console).We then perform the asynchronous operations that change the message (clicking a button or whatever 🖱🔨), and then waiting for the message to change.

Using these waitFor functions we can detect when something in our page changes after an async operation, now we just need to retrieve the data we want to test.

Retrieving data from selectors after an Async operation

Once we have detected the changes caused by our asynchronous code, we typically want to extract some data from our application that we can later compare to the expected visual result from a user interaction. We do that using evaluate(). The most common cases that you face when retrieving data are:

Figure 2: Most of the times, when testing the result of your asynchronous call

- Checking that a DOM element has appeared

A pretty common case is checking that a given element has been rendered on the page, hence appearing on the DOM. For instance, after saving a post, you should find it in the saved posts section. Navigating there and querying if indeed the DOM element is there is the basic type of data that we can assert (as a boolean assertion).

Find below an example of checking if a given post with an id post-id, where the id is a number we know, is present on the DOM. First we save the post, we wait for the post to be saved, go to the saved posts list/route and see that the post is there.

In there, we can observe a couple of things.

  1. The aforementioned need to have unique ids for the tested elements. Given so, querying the selector is way easier and we do not need to do nested queries that get the element based on its position on the DOM (Hey, get me the first tr element from the first row of that table 🙋🏼).
  2. We see how we can pass arguments to the evaluate function and use it to interpolate variables into our selectors. As the function is being evaluated in another scope, you need to bind the variables from node to that new scope, and you can do that via that second parameter.

- Checking for matching property values (e.g innerHTML, option…)

Now imagine that instead of checking that an element is on the DOM, you actually want to check if the list of saved posts rendered on the page actually are the posts you have saved. That is, you want to compare an array of strings with the post names, e.g ["post1,"post2"], with the saved posts of a user (which you can know beforehand for a test user, or retrieve from the server response).

For doing that, you need to query all the title elements of the posts and obtain a given property from them (as it could be their innerHTML, value, id…). After that, you have to convert that information to a serializable value (the evaluate function can only return serializable values or it will return null, that is, always return arrays, strings or booleans, for instance, not HTMLElements…).

An example performing that testing would be:

Those are the most basic cases (and the ones you will be using most of the time) for testing the data of your application.

-Asserting that the waitFor is succesful

Another thing you can do instead of evaluate() is, in case you just want to assert a boolean selector or a particular DOM change, is just assign the waitFor() call to a variable and check if it is true. The downside of that method is that you will have to set an estimated timeout to the function that is less than the Jest timeout set at the start. ⏳

If that timeout is exceeded the test will fail. It requires you to put an estimate timeout that you think is enough for the element to be rendered on your page after the request is made (Hmm yeah, I think that around 3 seconds should be enough…🤔).

For example, we want to check if a new tag has been added to a post querying the number of tag elements present before and after adding tags, that is, comparing their length and see if it has increased, denoting that the tag has indeed been added.

Note that you also have to bind the variables to the waitForFunction() as the third element if you specify the waitForFunction parameter as a closure.

In order to get the data to compare with the information we have retrieved from the page, i.e. our ground truth, an approach is to have a controlled test user in which we know what to expect for each one of the tests (number of liked posts, written posts). In this approach we can then hardcode 😱 the data to expect, such as the post titles, number of likes, etc… like we did on the examples of the previous section

You can also fake the response data from the server. That way you can test that the data obtained from the back-end is consistent with what is rendered into the application by responding predictably, inmutable data which you know beforehand. This serves for testing if the application responds predictably (parses correctly) to the data returned from the server for a given call.

On the next section we will see how to hijack the requests and provide custom data which you know. Puppeteer provides a method to achieve that, but if you want to dwelve more into faking XMLHttpRequest and pretty much all the data your test manages, you should take a look into Sinon.js 💖

Intercepting Requests and faking Requests with Puppeteer

Imagine that you want to check if the list of saved posts rendered on the page is indeed correct given a list of saved posts for a certain user that we can obtain from an endpoint called /get_saved_posts. To enable requestInterception on puppeteer we just have to set when launching puppeteer

await page.setRequestInterceptionEnabled(true);

With that, we can set all the request to intercept and mock the response data. Of course, that requires knowing the structure of the data returned by the back-end. Typically, one would store all the fake data response objects into a separate class and then, on request interception, query the endpoint called and return the corresponding data. This can be done like that, using the page.on() function:

Disclaimer: For the API we assume the somewhat typical format https://api.com/v1/endpoint_name, so the parsing to retrieve the endpoint is specific to that format for exemplification purposes, you can detect the Request made based on other parameters of course, your choice 💪

You can see the full documentation for the Request class here

One can easily see that, depending on the size of your API, this can be quite complex, and also one of the charms of the e2e testing is to also test if the back-end is giving the correct information and data 😵.

All these methods require for you to enter known data, hardcoded as a response or as a variable to compare. Another way to get the data to compare is to intercept the response and store the data into the variable.

Getting the data to assert from the Response

You can also intercept responses and get the data from there. For instance, we can intercept the get_saved_posts response and store all the posts into a variable.

✍🏼 Note: The page.on() methods for request and response are typically placed on the beforeAll() method of your tests, after declaring the page variable, as they define a global behavior of your test.

So after your application has rendered everything you can query the DOM elements and then compare with the posts variable to see that your app effectively renders everything as expected.

Summary

In this article we provided a comprehensive overview on how to test applications that present asynchronous data fetched from a server, by using Puppeteer + Jest.

We have learned how to implement waiting for certain events to happen (e.g. DOM mutations), that trigger visual changes caused by our asynchronous data. We have gone through the pipeline of detecting, querying and comparing those changes with known data so we can assess that the application works as expected from a user perspective.

Got any questions?! Go out here, start implementing tests like a madman and pray for them to pass, happy coding! 🙇🏻

--

--

Albert Alises
QMENTA Tech Blog

Tech Lead | Software Developer at Kiwi.com, previously at QMENTA Inc. MSc. in Computational Biomedical Engineering. AR/VR, Web Development, GraphQL,and whatnot.