JavaScript Testing

Mocking API calls and simulating React components interactions

Marcin Wanago
JavaScriptTesting

A common thing is for our application to fetch some data from the API. A problem with it is that might fail for various reasons, such as the API being down. We want our tests to be reliable and independent and to ensure that we can mock some of our modules.

Mocking

For the purpose of providing an example, we modify our ToDoList component to be a smart component.

app/components/ToDoList.component.js
1import React, { Component } from 'react';
2import Task from "../Task/Task";
3import axios from 'axios';
4 
5class ToDoList extends Component {
6  state = {
7    tasks: []
8  }
9  componentDidMount() {
10    return axios.get(`${apiUrl}/tasks`)
11      .then(tasksResponse => {
12        this.setState({
13          tasks: tasksResponse.data
14        })
15      })
16      .catch(error => {
17        console.log(error);
18      })
19  }
20  render() {
21    return (
22      <div>
23        <h1>ToDoList</h1>
24        <ul>
25          {
26            this.state.tasks.map(task =>
27              <Task key={task.id} id={task.id} name={task.name}/>
28            )
29          }
30        </ul>
31      </div>
32    )
33  }
34}
35 
36export default ToDoList;

It fetches the data using axios, therefore, we need to mock that module, because we don’t want actual requests to be made. Such mocks are defined in a __mocks__ directory where the filename is treated as a name of the mocked module.

__mocks__/axios.js
1'use strict';
2module.exports = {
3  get: () => {
4    return Promise.resolve({
5      data: [
6        {
7          id: 0,
8          name: 'Wash the dishes'
9        },
10        {
11          id: 1,
12          name: 'Make the bed'
13        }
14      ]
15    });
16  }
17};
If you want to mock some of the core modules of Node (for example fs or path) you need to explictly call jest.mock(‘moduleName’) in the mock file

Jest allows us to spy on functions:  let’s now test if the get function that we created is called.

app/components/ToDoList.test.js
1import React from 'react';
2import { shallow } from 'enzyme';
3import ToDoList from './ToDoList';
4import axios from 'axios';
5 
6jest.mock('axios');
7 
8describe('ToDoList component', () => {
9  describe('when rendered', () => {
10    it('should fetch a list of tasks', () => {
11      const getSpy = jest.spyOn(axios, 'get');
12      const toDoListInstance = shallow(
13        <ToDoList/>
14      );
15      expect(getSpy).toBeCalled();
16    });
17  });
18});

Thanks to calling  jest.mock('axios') Jest replaces axios with our mock – both in the test and the component.

The spyOn function returns a mock function. For a full list of its functionalities visit the documentation. Our test checks if the components call the get function from our mock after rendering and running it will result with a success.

1PASS  app/components/ToDoList/ToDoList.test.js
2  ToDoList component
3    when rendered
4      ✓ should fetch a list of tasks

If you are spying on your mocked functions in more than one test, remember to clear mock calls between each test, for example by running  getSpy.mockClear(). Otherwise, the number of function calls would persist between tests. You can also make it a default behavior by adding this snippet in your package.json file:

1"jest": {
2  "clearMocks": true
3}

Mocking Fetch API

Another common situation is using Fetch API. A trick to it is that it is a global function attached to the window object and to mock it, you can attach it to the global object. First, let’s create our mocked fetch function.

__mock__/fetch.js
1export default function() {
2  return Promise.resolve({
3    json: () =>
4      Promise.resolve([
5        {
6          id: 0,
7          name: 'Wash the dishes'
8        },
9        {
10          id: 1,
11          name: 'Make the bed'
12        }
13      ])
14 
15  })
16}

Then, let’s import it in the setupTests.js file.

app/setupTests.js
1import Adapter from 'enzyme-adapter-react-16';
2import { configure } from 'enzyme';
3import fetch from './__mocks__/fetch';
4 
5global.fetch = fetch;
6 
7configure({adapter: new Adapter()});
Please note that you need to provide the path to the setupTests.js file in the package.json – it is covered in the second part of the tutorial.

Now you are free to use fetch in your components: thanks to our mock, it will now be available.

1componentDidMount() {
2  return fetch(`${apiUrl}/tasks`)
3    .then(tasksResponse => tasksResponse.json())
4    .then(tasksData => {
5      this.setState({
6        tasks: tasksData
7      })
8    })
9    .catch(error => {
10      console.log(error);
11    })
12}

When setting up a spy, remember to set it to the window.fetch

app/components/ToDoList.test.js
1describe('ToDoList component', () => {
2  describe('when rendered', () => {
3    it('should fetch a list of tasks', () => {
4      const fetchSpy = jest.spyOn(window, 'fetch');
5      const toDoListInstance = shallow(
6        <ToDoList/>
7      );
8      expect(fetchSpy).toBeCalled();
9    });
10  });
11});

Simulating React components interactions

In the previous articles we’ve mentioned reading the state or props of the component, but this is the time to actually interact with it. For that purpose of explaining it, let’s add a functionality of adding new tasks to our ToDoList.

app/components/ToDoList.js
1import React, { Component } from 'react';
2import Task from "../Task/Task";
3import axios from 'axios';
4 
5class ToDoList extends Component {
6  state = {
7    tasks: [],
8    newTask: '',
9  }
10  componentDidMount() {
11    return axios.get(`${apiUrl}/tasks`)
12      .then(taskResponse => {
13        this.setState({
14          tasks: taskResponse.data
15        })
16      })
17      .catch(error => {
18        console.log(error);
19      })
20  }
21  addATask = () => {
22    const {
23      newTask,
24      tasks
25    } = this.state;
26    if(newTask) {
27      return axios.post(`${apiUrl}/tasks`, {
28        task: newTask
29      })
30        .then(taskResponse => {
31          const newTasksArray = [ ...tasks ];
32          newTasksArray.push(taskResponse.data.task);
33          this.setState({
34            tasks: newTasksArray,
35            newTask: ''
36          })
37        })
38        .catch(error => {
39          console.log(error);
40        })
41    }
42  }
43  handleInputChange = (event) => {
44    this.setState({
45      newTask: event.target.value
46    })
47  }
48  render() {
49    const {
50      newTask
51    } = this.state;
52    return (
53      <div>
54        <h1>ToDoList</h1>
55        <input onChange={this.handleInputChange} value={newTask}/>
56        <button onClick={this.addATask}>Add a task</button>
57        <ul>
58          {
59            this.state.tasks.map(task =>
60              <Task key={task.id} id={task.id} name={task.name}/>
61            )
62          }
63        </ul>
64      </div>
65    )
66  }
67}
68 
69export default ToDoList;

As you can see, we use axios.post here. This means we need to expand our axios mock.

__mocks__/axios.js
1'use strict';
2 
3let currentId = 2;
4 
5module.exports = {
6  get: (url) =&gt; {
7    return Promise.resolve({
8      data: [
9        {
10          id: 0,
11          name: 'Wash the dishes'
12        },
13        {
14          id: 1,
15          name: 'Make the bed'
16        }
17      ]
18    });
19  },
20  post: (url, data) {
21    return Promise.resolve({
22      data: {
23        task: {
24          name: data.task,
25          id: currentId++
26        }
27      }
28    });
29  }
30};
I’ve introduced the currentId variable because we want to keep our IDs unique

Since we’ve got that out of our mind, let’s get to testing: the first thing to test is to check if modifying the input value changes our state.

app/components/ToDoList.test.js
1import React from 'react';
2import { shallow } from 'enzyme';
3import ToDoList from './ToDoList';
4 
5describe('ToDoList component', () => {
6  describe('when the value of its input is changed', () => {
7    it('its state should be changed', () => {
8      const toDoListInstance = shallow(
9        <ToDoList/>
10      );
11 
12      const newTask = 'new task name';
13      const taskInput = toDoListInstance.find('input');
14      taskInput.simulate('change', { target: { value: newTask }});
15 
16      expect(toDoListInstance.state().newTask).toEqual(newTask);
17    });
18  });
19});

A crucial thing here is the simulate function call. It is a function of the ShallowWrapper that we’ve mentioned a few times now. We use it to simulate events. The first argument is the type of the event (since we use onChange in our input, we should use change here), and the second one is a mock event object.

To take things further, let’s test if an actual post request gets send from our component after the user clicks the button.

1import React from 'react';
2import { shallow } from 'enzyme';
3import ToDoList from './ToDoList';
4import axios from 'axios';
5 
6jest.mock('axios');
7 
8describe('ToDoList component', () => {
9  describe('when the button is clicked with the input filled out', () => {
10    it('a post request should be made', () => {
11      const toDoListInstance = shallow(
12        <ToDoList/>
13      );
14      const postSpy = jest.spyOn(axios, 'post');
15 
16      const newTask = 'new task name';
17      const taskInput = toDoListInstance.find('input');
18      taskInput.simulate('change', { target: { value: newTask }});
19 
20      const button = toDoListInstance.find('button');
21      button.simulate('click');
22 
23      expect(postSpy).toBeCalled();
24    });
25  });
26});

Thanks to our mock and simulating events, the test passes!

Now things will get a bit trickier. We will test if the state updates with our new task. The interesting part is that the request is asynchronous.

1import React from 'react';
2import { shallow } from 'enzyme';
3import ToDoList from './ToDoList';
4import axios from 'axios';
5 
6jest.mock('axios');
7 
8describe('ToDoList component', () => {
9  describe('when the button is clicked with the input filled out, the new task should be added to the state', () => {
10    it('a post request should be made', () => {
11      const toDoListInstance = shallow(
12        <ToDoList/>
13      );
14      const postSpy = jest.spyOn(axios, 'post');
15 
16      const newTask = 'new task name';
17      const taskInput = toDoListInstance.find('input');
18      taskInput.simulate('change', { target: { value: newTask }});
19 
20      const button = toDoListInstance.find('button');
21      button.simulate('click');
22 
23      const postPromise = postSpy.mock.results.pop().value;
24 
25      return postPromise.then((postResponse) => {
26        const currentState = toDoListInstance.state();
27        expect(currentState.tasks.includes((postResponse.data.task))).toBe(true);
28      })
29    });
30  });
31});

As you can see, the postSpy.mock.results is an array of all the results of the post function and by using it, we can get to the promise that is returned: it is available in the value property.

Returning a promise from the test is a way to make sure that Jest waits for it to resolve.

Summary

In this article, we covered mocking modules and used it to fake API calls. Thanks to not making actual requests, our tests can be more reliable and faster. Aside from that, we’ve simulated events throughout the React component. We’ve checked if it resulted in an expected outcome, such as a request made by the component or the state changing. To do that, we’ve learned the concept of a spy.