High ROI Testing with React, Enzyme and Jest – Part 5, Testing More Complex Async Code

By Ryan Vice | Posted on September 8, 2018

Testing

Overview

In this 5th post in this series we are going to look at writing tests for more complex asynchronous call patterns that we added in the 4th post in this series.

NOTE: the starting point code for this post is located here: https://github.com/RyanAtViceSoftware/react-testing-best-practices/tree/step6-adding-complex-async-calls

How Requirements Have Changed

Now that our requirements have changed we have to update our existing test to the new requirements. There are several things that are now different in the behavior of our app including:

  1. We no longer fetch posts when the page first loads
  2. We now wait till the user clicks Get Posts button before we fetch posts
  3. We expect the user to provide a User Name
  4. We will call /users?username=<UserName> to get the user’s details
  5. We will then call /posts?userid=<userId> and pass the returned userid from our previous call to /users

Fixing the “Loading Indicator” Test

First thing we need to do is add code that clicks the Get Posts button as shown below.

it('Then it shows a loading indicator', () => {
    http.get = sinon.stub();

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

    const app = mount(<App/>);

    const button = app.find('button');

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

    button.simulate('click');

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

    const loadingIndicator = app.find('h3');

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

    expect(
      loadingIndicator
        .getElements()[0]
        .props.children
    ).toBe('Loading...');
  });

Here we

  1. Query our button from the VDom using app.find('button')
  2. Verify that our query returned something using expect(app.getElements().length).toBeTruthy();
  3. Click our button using button.simulate('click');

Rerunning this test shows the following failure.

This makes sense if you think about how we changed our code. We’ve wired up our button as shown below.

<button onClick={this.getPosts}>Get Posts</button>

Now clicking our button will call getPosts which is shown below.

getPosts(e) {
    e.preventDefault();

    this.setState({ fetching: true });

    getUserByUserName(this.state.username)
      .then(user => user.length && user[0].id)
      .then(getPostsByUserId)
      .then(posts => this.setState({
        posts: posts,
        fetching: false,
        error: null
      }))
      .catch(e => {
        this.setState({
          error: e.message,
          fetching: false
        });
      });
  }

Our error indicates that line 22 is not returning a promise and if we look at our http mocking code we will see that we aren’t mocking /users yet so it makes sense that we’d not be getting a promise in the code under test. So let’s add mocking to return users and see what happens.

it('Then it shows a loading indicator', () => {
    http.get = sinon.stub();
    const username = 'Bret';

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

    http.get
      .withArgs('/users', {
        params: {
          username
        }
      })
      .returns(Promise.resolve(getDummyUser()));

    const app = mount(<App/>);
    
    const userNameInput = app.find('input');

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

    userNameInput.simulate('change', { target: { value: username}});

    const button = app.find('button');

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

    button.simulate('click');

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

    const loadingIndicator = app.find('h3');

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

    expect(
      loadingIndicator
        .getElements()[0]
        .props.children
    ).toBe('Loading...');
  });

Rerunning our test show that it is now passing as shown below.

How it works is that we

    1. Added a mock for /users that expects the user name to be passed on the query string as shown below
      http.get
            .withArgs('/users', {
              params: {
                username
              }
            })
            .returns(Promise.resolve(getDummyUser()));
    2. Added a user name to our text input using Enzyme’s simulate method as shown below
      const userNameInput = app.find('input');
      
          expect(app.getElements().length).toBeTruthy();
      
          userNameInput.simulate('change', { target: { value: username}});

Fixing Our “Posts Get Written To the Screen” Test

Our other failing tests will obviously need the same updates to get it consistent with the new requirements. So we can copy the code from the above test into it as shown below.

describe('When we have posts on the server ',
    () => {
      describe('And we enter a username into our Username textbox',
        () => {
          describe('And we click the Get Posts button',
            () => {
              it('Then we get posts written to the screen',
                  done => {
                http.get = sinon.stub();

                const username = 'Bret';

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

                http.get
                  .withArgs('/users', {
                    params: {
                      username
                    }
                  })
                  .returns(Promise.resolve(getDummyUser()));

                const app = mount(<App/>);

                const userNameInput = app.find('input');

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

                userNameInput.simulate('change', { target: { value: username}});

                const button = app.find('button');

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

                button.simulate('click');

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

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

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

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

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

And now when we run our test they pass as shown below.

Let’s Refactor!

Now that we have our tests passing let’s clean up our test code. We have the following problems.

DRY

Our first problem is that our tests have repeated code so let’s refactor them to share that code.

DRY Part 1 – Extracting Common Code

First let’s pull the shared code out into shared functions as shown below.

function mountApp() {
  http.get = sinon.stub();
  const username = 'Bret';

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

  http.get
    .withArgs('/users', {
      params: {
        username
      }
    })
    .returns(Promise.resolve(getDummyUser()));

  const app = mount(<App/>);
  return Promise.resolve({app, http, username});
}

function addUserNameAndClickGetPosts({app, http, username}) {
  const userNameInput = app.find('input');

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

  userNameInput.simulate('change', { target: { value: username}});

  const button = app.find('button');

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

  button.simulate('click');

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

  return Promise.resolve({app, http});
}

What we’ve done here is

  1. Break our test setup code into two functions mountApp and addUserNameAndClickGetPosts
  2. We have each of our function return the things needed by the next steps in the tests using Promise.resolve

DRY Part 2 – Updating Loading Indicator

The reason we’ve done this is a style preference that we’ve settled on over time. I’ll review why and the benefits in a bit but for now let’s see how we can consume this code in our Loading Indicator test as shown below.

it('Then it shows a loading indicator', done => {
    mountApp()
      .then(addUserNameAndClickGetPosts)
      .then(({app}) => {
        const loadingIndicator = app.find('h3');

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

        expect(
          loadingIndicator
            .getElements()[0]
            .props.children
        ).toBe('Loading...');
      })
      .then(() => done())
      .catch(done.fail);
  });

This is purely a style preference but the first benefit I like of this approach is that it allows for you to compose your tests of reusable steps that you can chain together using the Promise .then method. I think this makes the test really readable especially when mixed with named functions that allow hoisting. Combining these two approaches allows you to have the first few lines of your test be very descriptive about what the tests does and then you can dive into the function to find the details. I personally like having this table of contents at the top of my test functions.

A few things to note about the new code:

  1. We’ve made our test asynchronous by adding done as a parameter to our function
  2. We’ve added a .then(() => done()) after our test code which will call done after the test code runs with no exceptions allowing Jest to know our test passed
  3. We’ve added a .catch(done.fail) to our test so that if an exception is thrown it won’t be swallowed giving a false positive and will instead fail the test

NOTE: we are calling .then(() => done()) and not .then(done) because if we accidentally pass an argument to Jest’s done function the test will blow up with a very confusing error and hours will be lost and tears will be shed.

Let’s improve our test a little more to take advantage of Jest’s support for promises as shown below.

it('Then it shows a loading indicator', async () => {
    return mountApp()
      .then(addUserNameAndClickGetPosts)
      .then(({app}) => {
        const loadingIndicator = app.find('h3');

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

        expect(
          loadingIndicator
            .getElements()[0]
            .props.children
        ).toBe('Loading...');
      });
  });

The things we have changed here are:

  1. On line 1 we changed our test function to be an async arrow function, async () =>,  instead of a function that takes a done callback function
  2. On line 2 we are returning the promise that is created by our mountApp() function
  3. We removed the .then(() => done()) and the .catch(done.fail) parts of our promise chain

The test will work exactly the same as before but here we are saving some typing by using Jest’s promise support which

  1. Will fail the tests if the promise chain reject‘s due to an explicit reject call or due to an exception being thrown
  2. Will pass the test if the promise chain completes successfully
  3. Allow us not to worry about accidentally passing an argument to done and loosing half a day as described in the note above

DRY Part 3 – Updating Get Posts

Let’s refactor our Get Posts test.

describe('When we have posts on the server ',
    () => {
      it('Then we get posts written to the screen',
        async () => {
          return mountApp()
            .then(addUserNameAndClickGetPosts)
            .then(({app}) => {
              setTimeout(() => {
                app.update();

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

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

Now our tests are way shorter and easier to follow and our shared code has been refactored into reusable steps that we can compose in other tests using promise chains.

NOTE: one other benefit of using promise chains is that each step will move the code in that step to the next tick of Javascript’s event loop which will save having to wrap code in setTimeout calls some of the time.

Let’s Make Our Tests Clear About The changes

We’ve cleaned up the shared code and our tests are passing but we still have a major problem. Our tests are still communicating the old requirements in the test output and in the describe and it parameters so let’s get those updated as our last step. Let’s update the code as shown below.

Here we have

    1. Inserted a new describe('When fill in a username and click Get Posts ',
    2. Updated our previous Then we have posts... to be And we have posts...

And this makes our test output look like below.

This is extremely important as the point of the tests is to test the behavior of the system and to make it easy for other developers to know when tests fail if they have broken something or if the test simply needs to be updated to new requirements (or behaviors) as we did here. If the behavior the tests expects isn’t clear then the developer will automatically think that the tests needs to be updated as the behavior described won’t match the current system behavior. At a minimum lot’s of time could be wasted and all this could be easily avoided by keeping the test behaviors updated in the describe and it calls.

In future posts we are going to look at applying these approaches to our server code.

NOTE: the final code from this part of the series is available here: https://github.com/RyanAtViceSoftware/react-testing-best-practices/tree/step7-adding-tests-for-complex-async-calls

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