Axios Mocking in React using Typescript and testing-library
Tags: typescript, axios, testing, react
Published: 2020-02-29 11:06:51

I got stuck on an issue yesterday, so I thought I'd create a post on my findings and solution. 

In the end, it was quite simple really (the longer I get stuck on something, the easier the solution turns out to be... 🤷‍♂️). My googlefu had failed me completely as every solution I found, was either out dated, or incomplete, or more importantly: did not account for Typescript. 

Important packages used here (github link at the end):
- [@testing-library/jest-dom v5.1.1](https://www.npmjs.com/package/@testing-library/jest-dom),
- [@testing-library/react v9.4.1](https://www.npmjs.com/package/@testing-library/react) 
- [ts-jest v25.2.1](https://www.npmjs.com/package/ts-jest)
- [jest v25.1.0](https://www.npmjs.com/package/jest)
- [axios v0.19.2](https://www.npmjs.com/package/axios)

Hmm. Where to start? Lets go with a **basic** useFetch hook, as that's where we use axios to fetch our data.

## useFetch axios hook

```javascript
export interface IUseFetch {
  response: any;
  loading: boolean;
  error: boolean;
}

export const useFetch = (run: boolean, url: string) => {
  const [response, setResponse] = useState({});
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);

  useEffect(() => {
    let mounted = true;
    const abortController = new AbortController();
    const signal = abortController.signal;
    if (run && mounted) {
      const fetchData = async () => {
        try {
          setLoading(true);
          const response = await axios.get(url);
          if (response.status === 200 && !signal.aborted) {
            setResponse(response.data);
          }
        } catch (err) {
          if (!signal.aborted) {
            setResponse(err);
            setError(true);
          }
        } finally {
          if (!signal.aborted) {
            setLoading(false);
          }
        }
      };
      fetchData();
    }

    return () => {
      mounted = false;
      abortController.abort();
    };
  }, [run, url]);

  return { response, loading, error };

}
```
Pretty standard useFetch hook. The `run` variable is the trigger for fetch to run. 

## App

Next up, our basic React component. This component is just an input, that does a search and  shows a div with some search results that come from our `useFetch` hook above.

```javascript

export interface ILocation {
  location: string;
  country: string;
}

export default function App() {
  const [searchString, setSearchString] = useState(""); 
  const [isPanelOpen, setIsPanelOpen] = useState(false); // show/hide results
  const [doSearch, setDoSearch] = useState(false); // controls fetch run

  // useFetch hook above.
  const { response, loading } = useFetch(doSearch, "test.json");

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearchString(e.target.value);
  };

  // If the searchString length > 0, then do the following
  useEffect(() => {
    setDoSearch(searchString.length > 0);
    setIsPanelOpen(searchString.length > 0);
  }, [searchString.length]);

  const renderSearchResults = () =>
    !loading &&
    !error &&
    response &&
    response.length > 0 && (
      <ul aria-label="search-results">
        {response.map((loc: ILocation, i: number) => (
          <li key={i}>
            {loc.location}, {loc.country}
          </li>
        ))}
      </ul>
    );

  return (
    <div className="App">
      <label htmlFor="search">Search:</label>
      <input
        type="text"
        aria-label="search-input" // label used by our tests
        id="search"
        name="search"
        autoComplete="off"
        value={searchString}
        onChange={handleChange}
      />

      {isPanelOpen && (
        <div aria-label="search-panel">{renderSearchResults()}</div>
      )}
    </div>
  );
}
```

Easy enough? Cool.

Now to the testing. 

## Testing

Before we start, looking at the code above, we have three aria-labels that we will use to assert.

- **search-input**: our input box
- **search-panel**: our search result container div. This may show empty depending on response (we won't cover that here)
- **search-results**: holds the actual json response from our useFetch hook

First, lets prepare our test file.

Create a file called `App.test.tsx` and set it up:

```javascript
import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect';
import React from 'react';
import {
  cleanup,
  render,
  fireEvent,
  wait,
} from '@testing-library/react';

import axios from 'axios';
import App from './App';

jest.mock('axios');

```

Things to note:

- `@testing-library/jest-dom/extend-expect`: allows us some useful extensions to jest-dom like `.toBeInTheDocument()`.
- We import axios normally No need for funny names.
- we do a standard `jest.mock('axios')` This lets our tests know that whenever they see an axios import, to replace it with a mock function.

###What you came here for: The Mock

Before we write our test, we mock. We're going to be mocking axios, and this was the part I was stuck on. But it's quite straightforward.

```javascript
import { AxiosResponse } from 'axios';

// load our test.json file. This can be copied to the local 
// folder. Can be a short version of your actual data set.
const testJson = require('../../test.json');

// Our mocked response
const axiosResponse: AxiosResponse = {
  data: testJson,
  status: 200,
  statusText: 'OK',
  config: {},
  headers: {},
};

// axios mocked
export default {
  // Typescript requires a 'default'
  default: {
    get: jest.fn().mockImplementation(() => Promise.resolve(axiosResponse)),
  },
  get: jest.fn(() => Promise.resolve(axiosResponse)),
};

```

What's happening here is that we create a mocked AxiosResponse, which contains all the essentials like the `response.status` which we use in our useFetch hook, and  then the most important part: the `response.data`.

Then we have the actual axios mock. Whenever our app sees the axios import, it'll use whatever is inside this. We're using `get` for this example, so I've included a `get` mock. The important thing to note here is that we have a `default` and this is used by Typescript. [More info here](https://github.com/kulshekhar/ts-jest/issues/120#issuecomment-283653644)

###The Test

Next, we write our test. In this test, we'll follow the recommendations [Kent C. Dodds has written about in his blogs](https://kentcdodds.com/). So we'll do just one end to end (E2E) test here. This will cover the user typing something into the input box, and seeing our search results.


```javascript

test("type text into input, and  display search results", async () => {
  // our test searchString
  const searchString = "syd";

  // Render App
  const { getByLabelText, queryByLabelText, debug } = render(<App />);

  // find the input
  const input = getByLabelText("search-input");

  // search panel should not be rendered at this point
  expect(queryByLabelText("search-panel")).not.toBeInTheDocument();

  // this fire the onChange event and set the value to 'syd'
  fireEvent.change(input, { target: { value: searchString } });

  // useFetch should be called to get data
  expect(axios.get).toHaveBeenCalled();

  // assert our input value to be searchString
  expect(input.value).toBe(searchString);

  // search panel is loaded in the document
  expect(queryByLabelText("search-panel")).toBeInTheDocument();

  // wait for search results to be rendered
  await wait(() => {
    expect(queryByLabelText("search-results")).toBeInTheDocument();
  });
});

```

We use `async` because need to `await` the search results to render.

And that's it. The key to mocking axios in typescript is just the mock file that returns an AxiosResponse. Then in our test we can assert `expect(axios.get).toHaveBeenCalled()`.


[Here's a link to a github repo](https://github.com/mnsr/react-typescript-axios-mock) as Codesandbox doesn't support jest.mock.

If you play around with it, you can see how it works by commenting the entire axios.ts file and jest.mock('axios') in the App.test.tsx file.

Hope this helps someone.