Asynchronous API testing in React
Testing asynchronous code in React can be a bit of a pain, particularly when that code contains API calls. There are any number of libraries that help with this situation, but for this post, I’ll be relying on Jest’s own mocking capabilities, as well as Enzyme for mounting.
Consider the following React Component:
What do we really care about when it comes to testing our
componentDidMount method? In my view, we should be testing that the
fetch call is made once the lifecycle method fires, that the
groceries are set in the component’s state, or that the
errorStatus is set in the event of an error. I don’t like having the naked fetch call here in the lifecycle method, but for now we’ll work around that.
If we run these tests right now, we’re not going to be pleased with the result. Jest isn’t going to actually make any fetch calls, and will instead spit out something along the lines of:
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Network request failed
The thing to remember is that we don’t actually care what the API is returning, we can’t really control that. We can only control what we expect it to return. With that in mind, Jest makes it easy to mock our fetch calls, and stub them with the expected data, using
mockImplementation. That way we can test that our application is digesting the data the way we want it to.
Now, our tests pass! Together, these tests ensure that we’re making a fetch call, and that both the happy and sad paths are tested. I’m not totally satisfied with how these tests look however; mocking the fetch calls in each method makes the tests harder to read. I could instead mock my fetch calls all at once, at the beginning of the file, using
In my opinion, the tests are more readable this way, as all the mocking happens in one place, at beginning of the file. The tradeoff is the tests now must run in order, or the test won’t be using the stubbed data that it’s expected.
Refactoring out fetch
I mentioned when we started writing our tests for App.js that I don’t like having the naked fetch calls in the
componentDidMount lifecycle method. The reason for this is twofold. If our app component grows even a little further, and we have to make another fetch call somewhere, we’ll have to update our mocking, making sure not to break any ordering. This could get cumbersome, as well as being hard to read. Furthermore, if we need to call the same endpoint again somewhere else in our code, we’ll have to repeat ourselves, rewriting the fetch call.
I prefer to keep my calls to external services in their own files. This makes it easier to test both the behavior or those
fetch's, as well as how my components will digest the data.
Here, I’ve pulled the
fetching of groceries into its own asynchronous method. Testing this in isolation is really easy, using a similar strategy as before. Now what we care about is that
fetchGroceries throws an error when it should, and returns data otherwise.
In the test, we’ll mock the implementation of the
fetch function, and assert that we get what we expect back from the method. Jest makes it easy (as of version 20+) to handle the output of an asynchronous function using
Pulling it together
Now that I have a reusable and tested method for fetching groceries, let’s use it in our React component.
Our tests for App.js still pass, since the
fetchGroceries method is calling out to
fetch, and we’ve mocked the implementation of it. However, it’d be better to mock out the implementation of
fetchGroceries now, so that we’re truly testing
componentDidMount in isolation. Jest makes it really easy to mock whole modules with
mock, which will also allow us to separate our tests and our mocks.
Line 5 above will look for a file named
apiCalls.js inside the
__mocks__ directory, and use that to override our methods with the same signature. We can then use the same
mockImplementationOnce strategy that we employed before:
Now on the first call of
fetchGroceries we will return a data object, and on the second call we’ll throw an error. If that’s at all confusing, realize that Jest is looking for the mock in relation to where the real file lives, in the
__mocks__ directory. For this project, the directory structure would look something like this:
As a rule of thumb, code is easier to test when it is doing less. By separating our API calls from component code, it’s easier to test the expected behavior of both pieces. By using the mocking and asynchronous expectations that are available in Jest, it’s easy to mimic the behavior of an API, and ensure that your application responds as you expect it should.