Testing API Request Hooks with Jest, Sinon, and react-testing-library
Tags: react, testing, hooks
Published: 2019-10-27 22:04:47

In this mini series we have developed a simple hook that abstracts away the logic of managing some common states used in data fetching. Now let's talk about testing.

I'm assuming that you are already familiar with the basics of unit testing React apps with Jest. If that's not the case, Jest's official document site is a great place to start: https://jestjs.io/docs/en/getting-started

## What To Test

Before we start writing any tests, we need to know what we need to test. This is a bit different from Test Driven Development (TDD) where we know what our desired outcomes are so we write tests first. But our tests should follow the same sets of rules, for example:

1. Test the outcomes, not the implementation. Treat the components/functions you are testing like black boxes - we feed it with data and check what we are getting back - try to avoid testing implementation details.
2. Tests should be isolated. A test should not affect other tests in any way, nor should it depend on any code inside another test.
3. Tests should be deterministic. Given the same input, a test should always give the same results.

Testing React components are usually pretty straight forward - we "render" the component (sometimes with props), and check if its output matches our expectations. If the component is interactive, we will simulate the user interactions (events) and see if it behaves correctly. 

Testing hooks is somewhat trickier, however with the same rules, we can confidently say:

1. For hooks that return values, we test if the expected values are returned for the same sets of inputs;
2. For hooks that provides actions (for example, `useState` returns a function that lets you change the `state` value), we can fire those actions and see if the outcome is correct;
3. For hooks that causes ["side effects"](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-functional-programming-7f218c68b3a0#4c36) - we will try to observe the side effect, and make sure that everything is cleaned up so other tests won't be affected.

Now let's take a quick look at our [`useGet` hook](https://dev.to/pallymore/clean-up-async-requests-in-useeffect-hooks-90h) - it:

1. Causes a side-effect: it sends a request over the network (using `fetch`)
1. Takes one parameter: `url` and returns values: `isLoading`, `data`, and `error`; The values changes based on the outcome of the side-effect it causes: when a request is pending, `isLoading` is true; when the request is successful, we will receive some `data`; if anything bad happens, `error` value will be populated. 
1. discards the first side-effect, if we provide a new value before `fetch` is resolved.

Now we know what to test!

## Mocking Async Requests

Now how do we observe the side-effect the hook is causing? Spinning up a server that responds to our testing requests sounds cumbersome - and the tests won't be isolated & deterministic - there could be network issues and they are going to make the tests fail; our tests will depend on the server to return correct responses, instead of user inputs/actions.

Luckily there are couple of mocking libraries that allow us to observe asynchronous requests and control their results. To test React apps, I usually prefers [`sinon`](https://sinonjs.org/) which provides a very easy API to setup fake request handlers and clean things up.

Here we will need to use its `fakeServer` method:

```js
import { fakeServer } from 'sinon';

// setup a fake server
// we will need to hold a reference to the server so we can tell it when/what to respond to requests (and clean it up later)
let server;

beforeEach(() => {
  server = fakeServer.create();
});
```

`sinon` doesn't really spin up a "server" that runs along side of our tests. Under the hood, it just fakes the native `XMLHttpRequest` so all of our outgoing requests are intercepted. This change is global - we want to make sure that one request fired in one tests won't interfere with a different test, so we need to remove the fake after each test:

```js
afterEach(() => {
  server.restore();
});
```

In our tests, we can tell the fake server how to handle each request, like so:

```js
server.respondWith('GET', url, [
  200,
  {},
  JSON.stringify(mockData),
]);
```

The code above tells our server that:

1. It accepts "GET" requests to the `url`
2. It should respond with status code `200` (OK)
3. It doesn't return any headers
4. The body of the response is `mockData` (as a string)

If we want a request to fail, we can just change the status code to `4xx` (e.g. `400` for "Bad Request",`403` for "Forbidden") or `5xx` (`500` for "Internal Server Error"), and provide an error message in the response body. 

`respondWith` is very flexible - you can find all the options, and all the stuff you can do [here](https://sinonjs.org/releases/latest/fake-xhr-and-server/#serverrespondwithresponse).

Often we don't want the server to respond right away, we can control when the server should respond by calling: `server.respond();`.

## Writing the Test

Hooks look like they are just plain old JavaScript functions, but if we call one directly outside a React component we are going to see this:

```
    Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component.
```

There are a couple of different ways to get around this - one of them is creating a simple function component that uses this hook, and we can test the rendered output of that component. It's not a bad solution honestly, however there is a much easier, and more elegant way - using [`@testing-library/react-hooks`](https://github.com/testing-library/react-hooks-testing-library). I'm fairly new to "@tesing-library" packages but I fell in love with this one immediately just after writing a few tests. 

To setup our hook, we can simply call `renderHook` like so:

```js
import { renderHook } from '@testing-library/react-hooks';

// ... test setup

const url = '/foo/bar';
const { result, waitForNextUpdate } = renderHook(() => useGet({ url }));
```

It returns lots of useful goodies, here we only need `result` and `waitForNextUpdate`.

* `result`, as its name suggests, is an object that holds the values that our hook returns;
* `waitForNextUpdate` is a function that allows us to wait until all async stuff our hook is doing. This is where this testing library really shines.

Now let's write our first test: we want to make sure that the initial states are as expected:

```js
it('returns proper initial states', () => {
  const url = '/foo/bar';
  const { result } = renderHook(() =>
    useGet({ url })
  );
  
  expect(result.current.isLoading).toEqual(true);
  expect(result.current.data).toBeNull();
  expect(result.current.error).toBeNull();
});
```

Isn't it easy? Now let's combine it with fake server - we want to make sure returns the data from the server when the request finishes.

```js
// note, this is an `async` test 
it('GETs data from the server', async () => {
  const url = '/foo/bar';
  const expectedData = { some: 'data' }; // we define some data the server will be returning
  // setup the server
  server.respondWith('GET', url, [
    200,
    {},
    JSON.stringify(expectedData),
  ]);

  // setup our hook
  const { result, waitForNextUpdate } = renderHook(() =>
    useGet({ url })
  );
  
  // just to make sure our data is still `null` at this point
  expect(result.current.data).toBeNull();

  // tell our server it's time to respond!
  server.respond();

  // magic! we will wait until our hook finishes updating its internal states;
  await waitForNextUpdate();

  // assert the outcomes! 
  expect(result.current.data).toEqual(expectedData);
  expect(result.current.isLoading).toEqual(false);
  expect(result.current.error).toBeNull();
});
```

Similarly we can test that it returns expected messages when the server responds with an error code.

How do we test the request cancellation bit? How do we provide the hook with a new url before we call `server.respond()`? I'm glad you asked 😄 `renderHook` also returns a `rerender` method that allows us to provide some props to the hook - the setup looks slightly different from the example above though:

```js
const initialUrl = '/first/request';
const { rerender } = renderHook(({ url }) => useGet({ url }), {
  initialProps: { url: initialUrl }
});
```

Now the function we provide to `renderHook` accepts a `url` prop which is in turn used in the `useGet` call. And with the second argument we are telling `renderHook` that the initial value of `url` should be `'/first/request'`. 

In order to re-run our hook with new props, we can simply do:

```js
rerender({ url: '/new/url' });
```

Putting it together, to write this test we will:

1. setup our server to respond to two URLs with different data
1. render the hook with an initialUrl 
2. `rerender` our hook with a new url
3. tell the fake server that it's time to send back responses 
4. assert that our result should only include data from the second call

Now you've got everything you need to write this test, would you accept this challenge?

Hint: You probably will need to use a different method to handle requests in order to resolve the second request before the first one. [Read the docs here](https://sinonjs.org/releases/v7.5.0/fake-xhr-and-server/).

## It's a Wrap

Thanks for reading my very first blog series on React & testing! React is a wonderful library to work with and its community is actively working to improve experiences of both the developers and the end-users. And hooks make things much easier to share common states / workflows within the codebase. I hope you find those posts helpful 🤗 and please stay tuned for more React best practices posts!