

React-Query useMutation with Jest Testing
source link: https://www.js-howto.com/react-query-usemutation-with-jest-testing/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Introduction
React-Query is a powerful tool for fetching/caching the data on the frontend side, yet testing React-Query with Jest and React-testing-library might be a bit tricky, in this article we’re going to demonstrate it and see how easily it can be done.
Following to our previous articles which i explained how to use React-Query useInfiniteQuery and covering it with unit testing with 100% coverage using Jest and React-testing-library and Nock/MSW.
In this article i will cover the React-Query useMutation hook and how to test it with Nock/MSW.
Prerequisites
This article will be dependent on a previous articles we published. It’s preferred to read those articles before continuing in this one.
Note: Full source code that have example for testing hooks using both libraries (Nock and MSW):
Let’s get started!
We gonna keep it simple, here’s the results of the expected UI, that contains the form inputs and a button to submit the form, and we’ve the list of the users below

To do that:
1- Add the following CSS snippet to the file index.css
.form-input {
margin: 10px 0;
padding: 5px 0;
}
.form-input label {
color: black;
font-weight: bold;
margin-right: 10px;
}
.form-input input {
height: 25px;
width: 20%;
padding-left: 10px;
font-size: 18px;
}
.submit-button {
width: 35%;
font-size: 18px;
font-weight: bold;
background-color:#ccc;
border: 0;
border-radius: 4px;
padding: 10px;
cursor: pointer;
margin-bottom: 20px;
}
2- let’s create a new file called CreateUser.tsx
and put the following code in it:
import React, { FormEvent } from 'react';
export default function CreateUser() {
const onSubmitHandler = async (form: FormEvent<HTMLFormElement>) => {
form.preventDefault();
const formTarget = (form.target as HTMLFormElement)
const formData = new FormData(formTarget);
const firstName = formData.get('firstName') as string;
const lastName = formData.get('lastName') as string;
const email = formData.get('email') as string;
console.log(firstName, lastName, email);
}
return (
<div style={{ margin: '20px' }} className='create-user-form'>
<h1>Create User:</h1>
<form onSubmit={onSubmitHandler}>
<div className='form-input'>
<label htmlFor='firstNameInput'>First Name: </label>
<input type="text" name="firstName" required id="firstNameInput" />
</div>
<div className='form-input'>
<label htmlFor='lastNameInput'>Last Name: </label>
<input type="text" name="lastName" required id="lastNameInput" />
</div>
<div className='form-input'>
<label htmlFor='emailInput'>Email: </label>
<input type="email" name="email" required id="emailInput" />
</div>
<br />
<button type='submit' className="submit-button">Submit</button>
</form>
<hr />
</div>
);
}
We’ve created a form UI, that contains the inputs for firstName, lastName, email
all of the fields are marked as required.
A Submit button is also added with type='submit'
, and we’ve attached a handler to handle the submit action to the form <form onSubmit={onSubmitHandler}>
In the onSubmitHandler
function, we’re parsing the formValues
as FormData
object, then we’re extracting the form values (firstName, lastName, email
) out of it.
3- Add CreateUser.tsx
to the index.tsx
file:
import ReactDOM from 'react-dom/client';
import { QueryClientProvider } from "react-query";
import App from "./App";
import CreateUser from './CreateUser';
import './index.css'
import reactQueryClient from "./queryClient";
const rootElement = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
rootElement.render(
<QueryClientProvider client={reactQueryClient}>
<CreateUser />
<App />
</QueryClientProvider>
);
4- Let’s add the logic for useMutation
hook to submit the form data to the backend.
Create a new file useCreateUserMutation.ts
, and add the following logic to it:
import { useMutation } from "react-query";
import axiosClient from "./axiosClient";
export const useCreateUserMutation = () => {
const mutation = useMutation(
(request: { firstName: string; lastName: string; email: string }) => {
return axiosClient({
url: `/data/v1/user/create`,
method: "POST",
data: request,
headers: {
"app-id": "62f43477f19452557ba1ce76",
},
});
}
);
return mutation;
};
We’ve used useMutation
hook, and defined the request body parameters
(request: { firstName: string; lastName: string; email: string })
so we can pass them later from the onSubmitHandler
.
5- Import the useUserCreateMutation
hook in the file CreatesUser.tsx
, it should results in the following implementation:
import React, { FormEvent, useState } from 'react';
import { useCreateUserMutation } from './useCreateUserMutation';
export default function CreateUser() {
const [error, setError] = useState('');
const { mutate, isLoading } = useCreateUserMutation();
const onSubmitHandler = async (form: FormEvent<HTMLFormElement>) => {
form.preventDefault();
const formTarget = (form.target as HTMLFormElement);
const formData = new FormData(formTarget);
const firstName = formData.get('firstName') as string;
const lastName = formData.get('lastName') as string;
const email = formData.get('email') as string;
if (!firstName) {
setError('Please enter a valid first name');
document.getElementById('firstNameInput')?.focus();
return;
}
if (!lastName) {
setError('Please enter a valid last name');
document.getElementById('lastNameInput')?.focus();
return;
}
if (!email) {
setError('Please enter a valid email');
document.getElementById('email')?.focus();
return;
}
mutate({
firstName,
lastName,
email
}, {
onSuccess: () => {
console.log('asdasd')
alert('User added successfully');
},
onError: (response) => {
alert('Failed to create user');
console.log(response);
}
})
}
return (
<div style={{ margin: '20px' }} className='create-user-form' data-testid="create-user-form">
<h1>Create User:</h1>
<form onSubmit={onSubmitHandler}>
<div className='form-input'>
<label htmlFor='firstNameInput'>First Name: </label>
<input type="text" name="firstName" id="firstNameInput" />
</div>
<div className='form-input'>
<label htmlFor='lastNameInput'>Last Name: </label>
<input type="text" name="lastName" id="lastNameInput" />
</div>
<div className='form-input'>
<label htmlFor='emailInput'>Email: </label>
<input type="email" name="email" id="emailInput" />
</div>
{
error && <div style={{ color: 'red', fontSize: '18px' }}>{error}</div>
}
<br />
<button type='submit' className="submit-button">Submit</button>
</form>
<hr />
</div>
);
}
At line 5, we’ve instantiated the useCreateUserMutation
hook, and destructed the
{ mutate, isLoading }
properties out of it.
At line 15, we’ve called the function mutate
and passed the form values as the first argument in shape of object, and the 2nd argument we’ve declared the onSuccess
and onError
handlers.
Also note we’ve added the attribute data-testid
over the form wrapper element, we will rely on this attribute in testing the UI component later, to make sure it’s renders correctly.
Now let’s try this in the browser, open the app and fill the form data and submit it, you should receive User added successfully
if everything went well.

Add user form
If you got the form submitted successfully, navigate in the users list, you should see the new item added there.

And in case of error, you will receive an alert with Failed to create user
message, and details about the error would be printed in the browser console.
Testing
Now we’ve done from creating the form and connecting it to the backend using useMutation hook, let’s jump into adding tests for both the hook and the form.
1- useCreateUserMutation Testing:
Create a new file under the __tests__
folder, named as useCreateUserMutation.test.tsx
import { renderHook } from '@testing-library/react-hooks';
import { act } from '@testing-library/react';
import nock from 'nock';
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { useCreateUserMutation } from '../useCreateUserMutation';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
describe("useCreateUserMutation", () => {
// 1- Adding test case for the happy path
it("should create new user", async () => {
const { result, waitFor } = renderHook(() => useCreateUserMutation(), {
wrapper: wrapper,
});
nock('https://dummyapi.io', {
reqheaders: {
'app-id': () => true
}
})
.post(`/data/v1/user/create`)
// Mocking the response with status code = 200
.reply(200, {});
act(() => {
result.current.mutate({
firstName: 'fTest',
lastName: 'lTest',
email: '[email protected]'
});
});
// Waiting for the request status to resolve as success, i.e: statusCode = 200
await waitFor(() => {
return result.current.isSuccess;
});
// Make sure the request status resolved to true
expect(result.current.isSuccess).toBe(true);
});
// 2- Adding test case for the sad path (i.e when no form data sent to the backend)
it("should return an error from the server", async () => {
const { result, waitFor } = renderHook(() => useCreateUserMutation(), {
wrapper: wrapper,
});
nock('https://dummyapi.io', {
reqheaders: {
'app-id': () => true
}
})
.post(`/data/v1/user/create`)
// 2- Mocking the response with status code = 200
.reply(400);
act(() => {
result.current.mutate({
firstName: '',
lastName: '',
email: ''
});
});
// Waiting for the request status to resolve as success, i.e: statusCode = 200
await waitFor(() => {
return result.current.isError;
});
// Make sure the request status resolved to true
expect(result.current.isError).toBe(true);
});
});
In this file, we’ve added two test cases, the 1st for the happy path of submitting a request to the backend with valid form data, And we’re expecting the request to resolve in success state, i.e: response status code is 200/201.
And for the 2nd case, we’ve added a test case for the sad path, meaning we’re submitting a request to the backend with wrong/incomplete form values, and the server respond with error status, i.e: status code is in the 400’s range.
You can add other test cases for sure to cover different permutations,
By running the test script for the above added file, you must see the following results:
PASS src/__tests__/useCreateUserMutation.test.tsx
useCreateUserMutation
✓ should create new user (34 ms)
✓ should return an error from the server (12 ms)
--------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------|---------|----------|---------|---------|-------------------
All files | 9.61 | 0 | 13.33 | 9.61 |
useCreateUserMutation.ts | 100 | 100 | 100 | 100 |
--------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.127 s
Ran all test suites matching /useCreateUserMutation.test.tsx/i.
Done in 2.23s.
2- CreateUser form Testing:
At this stage, we will test the UI view with mocking any server request same as we’ve done in the previous section.
Let’s create a new file under the __tests__
folder, named as CreateUser.test.tsx
In this file, we will create the queryClient and wrapper as we did before (you can extract them into a shared place and re-use them across different tests).
As well, since we’ve used window.alert
to notify us about the status of the form submissions (success/failure), we need to mock window.alert
function, let’s see in the example below:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import nock from 'nock';
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import CreateUser from '../CreateUser';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
// Create Jest mocked function and assign it to global.alert
const mockedAlert = jest.fn();
global.alert = mockedAlert;
describe('CreateUser', () => {
});
So nothing new in the above other than creating the mock function for window.alert.
Now, let’s create our first test case for testing the component gets rendered correctly,
it('renders', () => {
render(<CreateUser />, { wrapper });
expect(screen.getByTestId('create-user-form')).toBeInTheDocument();
});
We basically rendered the component inside the wrapper
, then we do assertion by querying the component by
data-testid
and check if it exists inside the document.
The next test case would be for testing the fields are getting rendered correctly as well,
describe('CreateUser', () => {
let firstName: HTMLInputElement;
let lastName: HTMLInputElement;
let email: HTMLInputElement;
let submitButton: HTMLButtonElement;
it('renders', () => {
render(<CreateUser />, { wrapper });
expect(screen.getByTestId('create-user-form')).toBeInTheDocument();
});
it('renders all of the fields', () => {
render(<CreateUser />, { wrapper });
firstName = screen.getByLabelText('First Name:');
lastName = screen.getByText('Last Name:');
email = screen.getByLabelText('Email:');
expect(firstName).toBeInTheDocument();
expect(lastName).toBeInTheDocument();
expect(email).toBeInTheDocument();
});
});
We’ve declared a variables to hold the DOM representation of the inputs/button elements inside the test suite, so that we can re-use them across different test cases.
Inside the 2nd test case we basically mount the component, then we query the input fields by their labels and checking if they are exists in the document.
Then next test case, would be for validating the form inputs before submitting the form, then filling the form with correct data and submit it.
Before diving into this test case, let’s make sure to update the @testing-library/user-event
to version 14.4.0, since the userEvents.setup function has an exporting issue and that was resolved in version 14.4.0. You can checkout more about the issue from github.
We also need to make sure to intercept any server request by Nock same as we did before.
To do that let’s update the package
yarn upgrade @testing-library/[email protected]
Now let’s add the test case implementation:
it('handles validation and submits the form successfully', async () => {
const user = userEvent.setup();
render(<CreateUser />, { wrapper });
firstName = screen.getByLabelText('First Name:');
lastName = screen.getByText('Last Name:');
email = screen.getByLabelText('Email:');
submitButton = screen.getByText('Submit');
// Click the submit button without filling the form
await user.click(submitButton);
// First name input is empty
expect(screen.getByText('Please enter a valid first name')).toBeInTheDocument();
// Fill first name input by any value
await user.type(firstName, 'JS');
await user.click(submitButton);
// Last name input is empty
expect(screen.getByText('Please enter a valid last name')).toBeInTheDocument();
// Fill last name input by any value
await user.type(lastName, 'User');
await user.click(submitButton);
// Email input is empty
expect(screen.getByText('Please enter a valid email')).toBeInTheDocument();
// Fill email input by any value
await user.type(email, '[email protected]');
// Mocking the backend request
nock('https://dummyapi.io', {
reqheaders: {
'app-id': () => true
}
})
.post(`/data/v1/user/create`)
// Mocking the response with status code = 200
.reply(200, {});
await user.click(submitButton);
await waitFor(() => {
expect(mockedAlert).toHaveBeenCalledWith('User added successfully')
});
});
So, we’ve mounted the component, then passed over the fields one by one, submit the form without filling any data would result in the message: Please enter a valid first name
Then after filling the first name value, we submit the form again, and expecting to get an error message of
Please enter a valid last name
, same happen when we fill the last name and try to submit the form without filling the email address, we will get the following message: Please enter a valid email
.
Then, after filling the email address with a correct value, we will have the form values are valid and we can submit the form, thus we’ve added Nock post request interception, and after submitting the form, we are validating that by having a test the checks if the mocked window.alert is being called with the value User added successfully
.
Now, let’s add the last test case which would be for submitting the form and expecting to receive an error from the backend, then we do confirm that by validating window.alert is being called with the message:
Failed to create user
it('submits the form with errors', async () => {
const user = userEvent.setup();
render(<CreateUser />, { wrapper });
firstName = screen.getByLabelText('First Name:');
lastName = screen.getByText('Last Name:');
email = screen.getByLabelText('Email:');
submitButton = screen.getByText('Submit');
// Fill form values
await user.type(firstName, 'JS');
await user.type(lastName, 'User');
await user.type(email, '[email protected]');
// Mocking the backend request
nock('https://dummyapi.io', {
reqheaders: {
'app-id': () => true
}
})
.post(`/data/v1/user/create`)
// Mocking the response with status code = 500
.reply(500, {});
await user.click(submitButton);
await waitFor(() => {
expect(mockedAlert).toHaveBeenCalledWith('Failed to create user')
});
});
Note in this case we’ve mocked request status 500 as internal server error.
Putting it all together:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import nock from 'nock';
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import CreateUser from '../CreateUser';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
const mockedAlert = jest.fn()
global.alert = mockedAlert;
describe('CreateUser', () => {
let firstName: HTMLInputElement;
let lastName: HTMLInputElement;
let email: HTMLInputElement;
let submitButton: HTMLButtonElement;
it('renders', () => {
render(<CreateUser />, { wrapper });
expect(screen.getByTestId('create-user-form')).toBeInTheDocument();
});
it('renders all of the fields', () => {
render(<CreateUser />, { wrapper });
firstName = screen.getByLabelText('First Name:');
lastName = screen.getByText('Last Name:');
email = screen.getByLabelText('Email:');
expect(firstName).toBeInTheDocument();
expect(lastName).toBeInTheDocument();
expect(email).toBeInTheDocument();
});
it('handles validation and submits the form successfully', async () => {
const user = userEvent.setup();
render(<CreateUser />, { wrapper });
firstName = screen.getByLabelText('First Name:');
lastName = screen.getByText('Last Name:');
email = screen.getByLabelText('Email:');
submitButton = screen.getByText('Submit');
// Click the submit button wihtout filling the form
await user.click(submitButton);
// First name input is empty
expect(screen.getByText('Please enter a valid first name')).toBeInTheDocument();
// Fill first name input by any value
await user.type(firstName, 'JS');
await user.click(submitButton);
// Last name input is empty
expect(screen.getByText('Please enter a valid last name')).toBeInTheDocument();
// Fill last name input by any value
await user.type(lastName, 'User');
await user.click(submitButton);
// Email input is empty
expect(screen.getByText('Please enter a valid email')).toBeInTheDocument();
// Fill email input by any value
await user.type(email, '[email protected]');
// Mocking the backend request
nock('https://dummyapi.io', {
reqheaders: {
'app-id': () => true
}
})
.post(`/data/v1/user/create`)
// Mocking the response with status code = 200
.reply(200, {});
await user.click(submitButton);
await waitFor(() => {
expect(mockedAlert).toHaveBeenCalledWith('User added successfully')
});
});
it('submits the form with errors', async () => {
const user = userEvent.setup();
render(<CreateUser />, { wrapper });
firstName = screen.getByLabelText('First Name:');
lastName = screen.getByText('Last Name:');
email = screen.getByLabelText('Email:');
submitButton = screen.getByText('Submit');
// Fill form values
await user.type(firstName, 'JS');
await user.type(lastName, 'User');
await user.type(email, '[email protected]');
// Mocking the backend request
nock('https://dummyapi.io', {
reqheaders: {
'app-id': () => true
}
})
.post(`/data/v1/user/create`)
// Mocking the response with status code = 500
.reply(500, {});
await user.click(submitButton);
await waitFor(() => {
expect(mockedAlert).toHaveBeenCalledWith('Failed to create user')
});
});
});
Executing the above test file would result in the following:
yarn test -- src/__tests__/CreateUser.test.tsx
PASS src/__tests__/CreateUser.test.tsx
CreateUser
✓ renders (18 ms)
✓ renders all of the fields (7 ms)
✓ handles validation and submits the form successfully (194 ms)
✓ submits the form with errors (133 ms)
--------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------|---------|----------|---------|---------|-------------------
All files | 47.69 | 38.09 | 40 | 47.69 |
CreateUser.tsx | 100 | 100 | 100 | 100 |
--------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 1.284 s
Ran all test suites matching /src\/__tests__\/CreateUser.test.tsx/i.
Done in 2.49s.
That’s it for React-Query useMutation with Jest Testing
And as always happy coding!
Photo from unsplash
Like this:
Recommend
-
18
Testing Flux Stores without JestThursday, February 19th 2015Flux stores present an interesting unit testing challenge. From
-
8
React Component Testing Guide: Jest and RTLMarch 14th 2021 5
-
11
React is an open-source framework for building reusable UI components and apps. It is used by thousands of developers around the world to create complex and multifaceted a...
-
10
Testing React app with JestIn this post we will look into how to write tests for react application using Jest Jest is open source testing framework built on top of JavaScript. It was majorly designed t...
-
6
PrerequisitesAn existing React app.You can find the full source code @ GitHub: https://github.com/alexadam/project-templates/tree...
-
18
Integrate Jest & React Testing Library in a React Vite Project. Integrate Jest & React Testing Library in a React Vite Project Install Dependencies...
-
4
Testing React Tracking with Jest and Enzyme By Matt Dole Apr 15, 2021 Recently, I needed to test a button that would make an analyt...
-
9
Testing React Native apps with JestHow to write unit and e2e tests for React Native apps using Jest in an Nx workspaceIn my previous
-
6
// Tutorial //How to Test a React App with Jest and React Testing LibraryPublished on May 9, 2022By Alyssa Holland
-
15
Instalación y configuracion de Jest + React Testing Library En proyectos de React + Vite Instalaciones: yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react yarn add --dev @te...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK