< Previous: Using Jest to Simulate User Interaction on a React Component   Next: Mocking Ajax with Jest: Making an Asynchronous Test Become Synchronous >

Time for Ajax: Using Jest with Asynchronous Tests

This is where things start to get a bit complicated. We're using Ajax in our Detail component, and the fourth test we're going to write will check that the Ajax call completes and sets us up with the 30 most recent forks on our selected project. This is complicated because Jest has no idea there's an Ajax call waiting to return, so if we write a test like this it will fail:

it('fetches forks from GitHub', () => {
    const rendered = TestUtils.renderIntoDocument(
        <Detail params={{repo: 'react'}} />
    );

    expect(rendered.state.forks.length).toEqual(30);
});

That loads our <Detail> component using the repo name 'react' (thus forcing it to behave like the user had browser to /detail/react), then immediately checks to see whether there are 30 forks available. This fails because the Ajax call is Asynchronous (that's what the first A in Ajax means) which means it won't stop other code from running while the data is being fetched.

There are a number of possible solutions to this problem, and we're going to look at two of them.

The first solution is to use Jest's waitFor() and runs() functions. The first of these, waitFor(), checks a condition once every 10 milliseconds to see whether the condition has become true. As soon as it becomes true, we can check our expected is present, which is where the runs() function comes in: code you run inside a runs() function will only execute once code inside the waitsFor() function completes.

The way this actually works is through anonymous functions, which makes it all very flexible. That is, waitsFor() will pause until the anonymous function you create returns true, which means you can check any number of complex conditions in there. Behind the scenes, waitFor() calls this function once every 10 milliseconds, and will carry on checking until it returns true.

There's a small catch, though: what if there's a network problem and it takes a minute for GitHub to return values? In this case, a collection of network tests might take hours to run, which is likely to cause problems. To make things easier, waitFor() lets you specify a timeout in milliseconds: if your anonymous function does not return true within that time, it's considered a failure and an error message is printed out.

Enough theory: time for some code. Add this test beneath the existing three:

__tests__/Detail-test.js

it('fetches forks from GitHub', () => {
    const rendered = TestUtils.renderIntoDocument(
        <Detail params={{repo: 'react'}} />
    );

    waitsFor(() => {
        console.log('In waitFor: ' + rendered.state.forks.length);
        return rendered.state.forks.length > 0;
    }, "commits to be set", 2000);

    runs(() => {
        expect(rendered.state.forks.length).toEqual(30);
    });
});

About half of that is the same as the broken example above, but note that I moved the expect() call to be inside runs() using an anonymous function – that's the () => { jumble of symbols.

The new part is the waitsFor() code, which again creates an anonymous function. This function does two things, it prints a message to the console, then checks whether our Detail has loaded any forks from GitHub. Save your file then run npm run test from the command line.

Now, the reason I had that console.log() call is so that you can see exactly how waitFor() works. The output from the test will be something like this:

In waitFor: 0
In waitFor: 0
In waitFor: 0
In waitFor: 0
In waitFor: 0
(many lines trimmed)
In waitFor: 0
In waitFor: 0
In waitFor: 0
In waitFor: 30
PASS  __tests__/Detail-test.js

Remember I said that waitsFor() calls your function once every 10 milliseconds? Well, there's your proof: every time you see In waitFor it's because your function is being called to check whether it returns true. In our test we use return rendered.state.forks.length > 0; which means "return true if fork has any items in it, otherwise return false."

When that function finally finds 30 items in rendered.state.forks, it returns true and waitFor() exits. Like I said, though, it's possible network gremlins creep in, which is where the second and third parameters for waitFor() come in: the second is "commits to be set" and the third is 2000. This tells waitFor() to wait up to a maximum of 2000 milliseconds (2 seconds), and if that time passes to fail with the message "timeout: timed out after 2000 msec waiting for commits to be set" – that last part is the text we provided.

That's all four of our tests written, but we're not done with testing just yet. Before continuing, I suggest you remove the console.log() statement from the fourth test otherwise it will get annoying.

If you place a console.log() call inside your waitFor() function you'll see it being called every 10 milliseconds.

Buy the book for $10

Get the complete, unabridged Hacking with React e-book and take your learning to the next level - includes a 45-day no questions asked money back guarantee!

If this was helpful, please take a moment to tell others about Hacking with React by tweeting about it!

< Previous: Using Jest to Simulate User Interaction on a React Component   Next: Mocking Ajax with Jest: Making an Asynchronous Test Become Synchronous >

Copyright ©2016 Paul Hudson. Follow me: @twostraws.