January 2, 2018

Mock Axios in React Jest+Enzyme Tests

React applications generated by create-react-app provide a pre-configured Jest test runner. Adding the Enzyme test utilities to the mix makes testing simple React components a breeze.

However, not all React components are simple, some may speak to a back-end API, using Axios or Fetch for example, to populate state for later rendering. Components such as these can be a challenge to test if your goal is to keep these tests isolated and fast. Ideally we want to eliminate network hops in tests.

This post will describe how to use Jest Mocks to simulate Axios in React component tests.

Example Application

For this article we will reference a simple React application. The main component, named BookList, will render a list of books fetched from an API endpoint.

A rough outline of the BookList component would be:

const BOOKS_ENDPOINT = 'http://localhost:3000/books.json';

class BookList extends Component {
  constructor(props) {
    super(props);

    this.state = {
      books: [],
    };
  }

  componentDidMount() {
    axios.get(BOOKS_ENDPOINT)
      .then(response => {
        this.setState({
          books: response.data.books
        });
      });
  }

  renderBooks() {
    return (
      this.state.books.map(book => <li key={book.id}>{book.title}</li>)
    );
  }

  render() {
    return (
      <div>
        <h1>Book List</h1>
        <ul>{this.renderBooks()}</ul>
      </div>
    );
  }
}

export default BookList;

Axios Mock

Since we do not want to speak to a back-end API in our tests we need to mock Axios. This is best done by creating an axios.js file in a top-level __mocks__ directory as follows:

import { data as books } from './books.json';

const BOOKS_ENDPOINT = 'http://localhost:3000/books.json';

module.exports = {
  get: jest.fn((url) => {
    switch (url) {
      case BOOKS_ENDPOINT:
        return Promise.resolve({
          data: books
        });
    }
  })
};

This mock uses a pre-cooked list of books stored in a JSON file books.json also stored in the __mocks__ directory:

{
  "data": {
    "books": [
      {
        "id": 1,
        "title": "ABC"
      },
      {
        "id": 2,
        "name": "DEF"
      }
    ]
  }
}

Note, the axios.js mock provides scope to return unique JSON responses for different URLS by simply extending the switch statement.

Component Test

An Ezyme snapshot test for a simple component would look something like:

  it('MyComponent snapshot test', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper).toMatchSnapshot();
  });

However, such a test will not work for an Axios based component, even our mocked version, since the retrieval of the books is an asynchronous operation done in the background. If the above test template where applied to out BookList component the snapshot would end up containing no books.

The test needs to wait for list of books to populate component state. That is best done by waiting for the Promise queue to be flushed as follows:

const flushPromises = () => new Promise(resolve => setImmediate(resolve));

it('BookList snapshot test', async () => {
  const wrapper = shallow(<BookList />);
  await flushPromises();
  wrapper.update();
  expect(wrapper).toMatchSnapshot();
});

We have marked our test function as being async; after we create the shallow component we await the flushing of the Promise queue before re-rendering the component with the update function. Now our snapshot will contain the list of books retrieved from the Axios mock.

This test will now correctly confirm that the BookList component can render a list of books retrieved from a back-end API whilst mocking away the network hop.

References

Most of the information of this post was gleamed from these bits ‘n pieces on the interwebs: