- 1. JavaScript testing #1. Explaining types of tests. Basics of unit testing with Jest
- 2. JavaScript testing #2. Introducing Enzyme and testing React components
- 3. JavaScript testing #3. Testing props, the mount function and snapshot tests.
- 4. JavaScript testing #4. Mocking API calls and simulating React components interactions
- 5. JavaScript testing #5. Testing hooks with react-hooks-testing-library and Redux
- 6. JavaScript testing #6. Introduction to End-to-End testing with Cypress
- 7. JavaScript testing #7. Diving deeper into commands and selectors in Cypress
- 8. JavaScript testing #8. Integrating Cypress with Cucumber and Gherkin
- 9. JavaScript testing #9. Replacing Enzyme with React Testing Library
- 10. JavaScript testing #10. Advanced mocking with Jest and React Testing Library
- 11. JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mocks
- 12. JavaScript testing #12. Testing downloaded files and file inputs with Cypress
- 13. JavaScript testing #13. Mocking a REST API with the Mock Service Worker
- 14. JavaScript testing #14. Mocking WebSockets using the mock-socket library
- 15. JavaScript testing #15. Interpreting the code coverage metric
- 16. JavaScript testing #16. Snapshot testing with React, Jest, and Vitest
- 17. JavaScript testing #17. Introduction to End-to-End testing with Playwright
- 18. JavaScript testing #18. E2E Playwright tests for uploading and downloading files
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import React, { Component } from 'react'; import Task from "../Task/Task"; import axios from 'axios'; class ToDoList extends Component { state = { tasks: [] } componentDidMount() { return axios.get(`${apiUrl}/tasks`) .then(tasksResponse => { this.setState({ tasks: tasksResponse.data }) }) .catch(error => { console.log(error); }) } render() { return ( <div> <h1>ToDoList</h1> <ul> { this.state.tasks.map(task => <Task key={task.id} id={task.id} name={task.name}/> ) } </ul> </div> ) } } export 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
'use strict'; module.exports = { get: () => { return Promise.resolve({ data: [ { id: 0, name: 'Wash the dishes' }, { id: 1, name: 'Make the bed' } ] }); } }; |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React from 'react'; import { shallow } from 'enzyme'; import ToDoList from './ToDoList'; import axios from 'axios'; jest.mock('axios'); describe('ToDoList component', () => { describe('when rendered', () => { it('should fetch a list of tasks', () => { const getSpy = jest.spyOn(axios, 'get'); const toDoListInstance = shallow( <ToDoList/> ); expect(getSpy).toBeCalled(); }); }); }); |
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.
1 2 3 4 |
PASS app/components/ToDoList/ToDoList.test.js ToDoList component when rendered ✓ 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 2 3 |
"jest": { "clearMocks": true } |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
export default function() { return Promise.resolve({ json: () => Promise.resolve([ { id: 0, name: 'Wash the dishes' }, { id: 1, name: 'Make the bed' } ]) }) } |
Then, let’s import it in the setupTests.js file.
app/setupTests.js
1 2 3 4 5 6 7 |
import Adapter from 'enzyme-adapter-react-16'; import { configure } from 'enzyme'; import fetch from './__mocks__/fetch'; global.fetch = fetch; configure({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.
1 2 3 4 5 6 7 8 9 10 11 12 |
componentDidMount() { return fetch(`${apiUrl}/tasks`) .then(tasksResponse => tasksResponse.json()) .then(tasksData => { this.setState({ tasks: tasksData }) }) .catch(error => { console.log(error); }) } |
When setting up a spy, remember to set it to the window.fetch
app/components/ToDoList.test.js
1 2 3 4 5 6 7 8 9 10 11 |
describe('ToDoList component', () => { describe('when rendered', () => { it('should fetch a list of tasks', () => { const fetchSpy = jest.spyOn(window, 'fetch'); const toDoListInstance = shallow( <ToDoList/> ); expect(fetchSpy).toBeCalled(); }); }); }); |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
import React, { Component } from 'react'; import Task from "../Task/Task"; import axios from 'axios'; class ToDoList extends Component { state = { tasks: [], newTask: '', } componentDidMount() { return axios.get(`${apiUrl}/tasks`) .then(taskResponse => { this.setState({ tasks: taskResponse.data }) }) .catch(error => { console.log(error); }) } addATask = () => { const { newTask, tasks } = this.state; if(newTask) { return axios.post(`${apiUrl}/tasks`, { task: newTask }) .then(taskResponse => { const newTasksArray = [ ...tasks ]; newTasksArray.push(taskResponse.data.task); this.setState({ tasks: newTasksArray, newTask: '' }) }) .catch(error => { console.log(error); }) } } handleInputChange = (event) => { this.setState({ newTask: event.target.value }) } render() { const { newTask } = this.state; return ( <div> <h1>ToDoList</h1> <input onChange={this.handleInputChange} value={newTask}/> <button onClick={this.addATask}>Add a task</button> <ul> { this.state.tasks.map(task => <Task key={task.id} id={task.id} name={task.name}/> ) } </ul> </div> ) } } export default ToDoList; |
As you can see, we use axios.post here. This means we need to expand our axios mock.
__mocks__/axios.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
'use strict'; let currentId = 2; module.exports = { get: (url) => { return Promise.resolve({ data: [ { id: 0, name: 'Wash the dishes' }, { id: 1, name: 'Make the bed' } ] }); }, post: (url, data) { return Promise.resolve({ data: { task: { name: data.task, id: currentId++ } } }); } }; |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import React from 'react'; import { shallow } from 'enzyme'; import ToDoList from './ToDoList'; describe('ToDoList component', () => { describe('when the value of its input is changed', () => { it('its state should be changed', () => { const toDoListInstance = shallow( <ToDoList/> ); const newTask = 'new task name'; const taskInput = toDoListInstance.find('input'); taskInput.simulate('change', { target: { value: newTask }}); expect(toDoListInstance.state().newTask).toEqual(newTask); }); }); }); |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import React from 'react'; import { shallow } from 'enzyme'; import ToDoList from './ToDoList'; import axios from 'axios'; jest.mock('axios'); describe('ToDoList component', () => { describe('when the button is clicked with the input filled out', () => { it('a post request should be made', () => { const toDoListInstance = shallow( <ToDoList/> ); const postSpy = jest.spyOn(axios, 'post'); const newTask = 'new task name'; const taskInput = toDoListInstance.find('input'); taskInput.simulate('change', { target: { value: newTask }}); const button = toDoListInstance.find('button'); button.simulate('click'); expect(postSpy).toBeCalled(); }); }); }); |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import React from 'react'; import { shallow } from 'enzyme'; import ToDoList from './ToDoList'; import axios from 'axios'; jest.mock('axios'); describe('ToDoList component', () => { describe('when the button is clicked with the input filled out, the new task should be added to the state', () => { it('a post request should be made', () => { const toDoListInstance = shallow( <ToDoList/> ); const postSpy = jest.spyOn(axios, 'post'); const newTask = 'new task name'; const taskInput = toDoListInstance.find('input'); taskInput.simulate('change', { target: { value: newTask }}); const button = toDoListInstance.find('button'); button.simulate('click'); const postPromise = postSpy.mock.results.pop().value; return postPromise.then((postResponse) => { const currentState = toDoListInstance.state(); expect(currentState.tasks.includes((postResponse.data.task))).toBe(true); }) }); }); }); |
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.
Hi,
Thank you for writing this up! This really helped me with understanding how to do mock api calls!
A question I have is..
What if you have a lot of get requests that you want to test? in the mocks file, would you create multiple mock “get” but with each having a different url?
Thanks!
how do you do this?
Awesome series.