4 stages you need to test with React Query + Testing Library

Jair Orlando Huaman Bellido
3 min readDec 20, 2021

When we code our unit tests for web application development, we start by checking if the component has rendered correctly, then we add scenarios about other expected behaviors. The difficulty of this increases when interacting with an API, and we assume that we will always receive a successful response but this is not always the case.

In this article, we will cover 4 stages you need to test when interact with an API using React Query and Testing Library.

Before to Start

Create a React + Typescript project

yarn create react-app my-app --template typescript

Add libraries

npm install react-query

Create a custom hooks which make the request to API.

import { Todo } from "./Todo.interface";
import { TodoAPI } from "./TodoAPI";
import { useQuery } from "react-query";
export default function useGetAllTodos() {
return useQuery<Todo[]>("fetchAll", () => TodoAPI.getAll());
}

An component for display data or another scenario from the API response.

import { Todo } from "./Todo.interface";
import useGetAllTodos from "./useGetAllTodos";
function isEmpty(arr: any[]): boolean {
return arr.length > 0 ? false : true;
}
export default function Todos() {
const { data, isError, isSuccess, isLoading } = useGetAllTodos();
return (
<div className="App">
{isSuccess && !isEmpty(data as Todo[]) && (
<div data-testid="data"><List /></div>
)}
{isSuccess && isEmpty(data as Todo[]) && (
<div data-testid="empty"><EmptyContainer /></div>
)}
{isError && <div data-testid="error"><ErrorMessage /></div>}
{isLoading && <div data-testid="loading"><Spinner /></div>}
</div>
);
}

A basic structure for our unit testing

import "@testing-library/jest-dom";
import useGetAllTodos from "./useGetAllTodos";
import { Todo } from "./Todo.interface";
import { QueryClient, UseQueryResult } from "react-query";
jest.mock("./useGetAllTodos");
const mockUseGetAllTodos = useGetAllTodos as jest.Mock<
UseQueryResult<Todo[], unknown>
>;
const todoMock: Todo[] = [
{
completed: true,
id: 1,
title: "Write a blog",
userId: 102,
},
];
const emptyTodoMock: Todo[] = [];describe("Todo Component", () => {
const queryClient = new QueryClient();
afterEach(() => {
jest.clearAllMocks();
});
});

Here is an article about the importance of “data-testid” attribute for testing. Making your UI tests resilient to change (kentcdodds.com)

1. Successfully

Let’s assume the API response successfully and we have to show data. We’ve the following testing scenario:

it("should show all todo items", () => {
mockUseGetAllTodos.mockImplementation((res) => ({
...res,
data: todoMock,
isLoading: false,
isError: false,
isSuccess: true,
}));
render(
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
expect(screen.getByTestId("data")).toBeInTheDocument();
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
expect(screen.queryByTestId("empty")).not.toBeInTheDocument();
expect(screen.queryByTestId("loading")).not.toBeInTheDocument();
});

2. Waiting

In this stage, we’re waiting a response from the API, and it’s important to show a user an indicator about this event, otherwise he will think that nothing is happening.

it("should show a loading component", () => {
mockUseGetAllTodos.mockImplementation((res) => ({
...res,
data: undefined,
isLoading: true,
isError: false,
isSuccess: false,
}));
render(
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
expect(screen.getByTestId("loading")).toBeInTheDocument();
expect(screen.queryByTestId("data")).not.toBeInTheDocument();
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
expect(screen.queryByTestId("empty")).not.toBeInTheDocument();
});

3. Error

The request failed and the user must know what happened in the application through an understandable message.

it("should show a message error when request failed", () => {
mockUseGetAllTodos.mockImplementation((res) => ({
...res,
data: undefined,
isLoading: false,
isError: true,
isSuccess: false
}));
render(
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
expect(screen.getByTestId("error")).toBeInTheDocument();
expect(screen.queryByTestId("loading")).not.toBeInTheDocument();
expect(screen.queryByTestId("data")).not.toBeInTheDocument();
expect(screen.queryByTestId("empty")).not.toBeInTheDocument();
});

4. Successfully, but empty data

The API response successfully, but it don’t send any data and it must show a component which represent it.

it("should show an empty container when not receive data", () => {
mockUseGetAllTodos.mockImplementation((res) => ({
...res,
data: emptyTodoMock,
isLoading: false,
isError: false,
isSuccess: true,
}));
render(
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
expect(screen.getByTestId("empty")).toBeInTheDocument();
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
expect(screen.queryByTestId("loading")).not.toBeInTheDocument();
expect(screen.queryByTestId("data")).not.toBeInTheDocument();
});

Result

Let’s see our final testing file:

Testing our components is a difficult job at first, but then we do it easily and ensure the integrity of the application.

Finally, it is important to apply these 4 scenarios in order to improve the user experience and avoid displaying blank screens.

Don’t be afraid testing ;)

If you enjoyed this article, please share with your friends and a virtual clap👏 for more posts.

Thank you!

--

--