High ROI Testing with React, Enzyme and Jest – Part 3, Writing an Async Test

By Ryan Vice | Posted on March 12, 2018

React| Testing

Overview

In this 3rd post in this series we are going to look at writing our first asynchronous test that test code which calls the server.

NOTE: the starting point code for this post is located here: https://github.com/RyanAtViceSoftware/react-testing-best-practices/tree/step-3-our-first-test

Writing the Test

Let’s start by adding the test below to our app.test.js file.

describe('Given we load our app ', () => {
  // other tests removed for clarity

  describe('When we have posts on the server ', () => {
    it('Then we get posts written to the screen',
        done => {
      http.get = sinon.stub();

      http.get
        .withArgs('/posts')
        .returns(Promise.resolve(getDummyPosts()));

      const app = mount(<App/>);

      expect(app.getElements().length).toBeTruthy();

      setTimeout(() => {
        app.update();

        const posts = app.find('li');

        expect(posts.getElements().length)
          .toBe(3);

        done();
      });
    });
  });
});

Then in the same file lets update our imports too add what’s shown below.

import App, { http } from './App';
import sinon from 'sinon';

Finally add the getDummyPost() helper function below that will return dummy data for the test to use.

function getDummyPosts() {
  return [
    {
      "userId": 1,
      "id": 1,
      "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
      "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
    },
    {
      "userId": 1,
      "id": 2,
      "title": "qui est esse",
      "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
    },
    {
      "userId": 1,
      "id": 3,
      "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
      "body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
    }
  ];
}

Now save app.test.js and run it and you should see output like below.

As we can see now we have a new test that is nested one more level deep using Gherkin Syntax. We now have our first When clause and we are now testing the use case below:

  • Give we load our app
    • When we have posts on the server
      • Then we get posts written to the screen

NOTE:Using Gherkin is just a recommendation here but I like Gherkin because it’s very easy to be consistent and to make the scenario under test extremely clear. Here we can clearly understand that we are testing that when we load the app if there are posts on the server then they are displayed to the user.

How It Works

The key to this approach is how we are stubbing the http.get method. First thing we do is import http object by updating the import as shown below.

import App, { http } from './App';

Because import is a CommonJs implementation when we use a named import in this way we get access to a reference to the singleton instance of the http object that is shared by our SUT (the App component). Because we have a reference to the same instance of the http object we can easily stub out methods on http by using the code shown below.

http.get = sinon.stub();

Now our SUT code will use the sinon.stub() version of the http.get() method.

NOTE: Sinon is a Javascript testing library provides testing helpers like stubs and mocks.

Here we are replacing http.get with a stub that we can then configure to return our dummy data only when it’s called with /posts url using the code below.

http.get.withArgs('/posts').returns(Promise.resolve(getDummyPosts()));

Note that we are using Promise.resolve() which will return a new promise that will immediately resolve and return the value we pass to it. In our case we are passing in the results of calling the test version of the getDummyPosts() function.

Now that we have stubbed http.get() we can mount our app as we did in our last test and verify that we get a ReactWrapper back from Enzyme as shown below.

const app = mount(<App/>);

expect(app.getElements().length).toBeTruthy();

How Do We Wait for the Async Code to Complete

At this point in our test we have to figure out a way to make our test code wait for the promise to resolve in our SUT that calls the server to get postsropse. Currently we are faking the server call using a Promise as shown below.

export const http = {
  get: (url, { dummyData } = {}) => 
    new Promise(resolve =>
      setTimeout(() => {
          resolve(dummyData);
        }, 1000
      )
    )
};

Note that I always add a wrapper around my AJAX lib like this http object in the projects I architect as it allows for:

  1. stubbing out APIs when needed
  2. having a simulated delay for stubbed APIs so you can test, both manually and automated, for both async spinners and other complex UI interactions
  3. it provides an architectural seam over your AJAX lib allowing you to swap it out if need and also allowing for standardizing things like error handling, logging, etc…

However, Javascript Promises have this quality to them as described on Mozilla.

What this means for our testing code is now we have to make any test code that comes after we mount our App component wait for our stubbed http.get() call’s promise to resolve. There is a lot of background information to understand the approach we are going to follow here and I recommend Daniel Parker’s excellent Javascript Promises book as a primer on promises and the Javascript Event Loop if you are new to them.

However, here we will provided a simplified explanation. Basically each time we call setTimeout() or use a promise the the callback code that we configure to be called will go to the end of the Javascript’s event loop’s queue for processing. In our code when the CUT calls http.get() our test code is already executing which means that our test code will run to completion before any callbacks are called so we can’t just put the code that would expect certain results after the async call returns right after our other test code as it would run before our async call returns. So what we have to do is push our test code that we want to run after any pending async callbacks to the bottom of the event loop’s queue. The easiest way I know to do this is using setTimeout() and passing 0 for the second parameter (which is the default value so we can omit the second parameter) as shown below.

setTimeout(() => {
        app.update();

        const posts = app.find('li');

        expect(posts.getElements().length)
          .toBe(3);

        done();
      });

Now our code from like 2 to 9 will be executed as the last thing in the event loop’s queue meaning our test code will wait for the CUT’s callbacks to fire before continuing.

The last piece of the puzzle here is getting Jest to wait for our async callbacks to complete and forturnately Jest supports async test. We do this by calling the done() method that will be passed as the first argument Jest’s it method as shown on line 9 above.

NOTE: the code for this point in this post is located here: https://github.com/RyanAtViceSoftware/react-testing-best-practices/tree/step-3-our-first-test

How Do We Know This Will Work with Code That Really Calls the Api

The easiest way to know this will work with the real code that calls the API is to add the real code and see if our goal of not having to change tests is achieved.

First lets run npm install axios --save to install axios which is a small library we can use for making AJAX calls to the server.

Next let’s update our http.get() call as shown below.

const baseUrl = `http://jsonplaceholder.typicode.com`;

export const http = {
  get: (url, config, { dummyData } = {}) =>
    dummyData ?
      new Promise(resolve =>
        setTimeout(() => {
            resolve(dummyData);
          }, 1000
        )
      )
      : axios.get(baseUrl + url, config).then(r => r.data)
};

What we have done here is to check to see if we have dummyData on line 5 and if we do then the code is the same as before and we fake the async call using the setTimeout() method and return dummyData. However, if we don’t specify dummyData then we will make a live call using axios.get() passing along the url and config that was passed into us. We are also prepending baseUrl which is declared immediately above http on line 1 above.

NOTE: in a real application we would extract all the http code and the configurations like the base url to separate files\modules.

The last thing we have to do to get this working is update our call to http.get() so that we are no longer passing in {dummyData: getDummyPosts()} which will cause us to call the live service as shown below.

function getPosts() {
  return http.get('/posts'); // we removed {dummyData: getDummyPosts()} 
}

If we run the code now we will see that the web app is displaying all the posts from the server as shown below.

And if we rerun the tests we will see that all tests are still passing.

Watch Video Series

5 Keys to Success Before Building Your Software

You may have considered getting a custom software system to help your business run more smoothly, but how do you get started? We will look at 5 factors that we feel are critical to success when building a custom software system.

Need an expert team of designers, mobile developers, web developers and back-end developers?

Discover Vice Software