In this article, we’ll discuss how to test React components where the data and data changes within the component depend on a GraphQL API. Before we dive in, let’s quickly refresh our understanding of GraphQL and the React library we’ll use to interact with a GraphQL API, React Apollo.
Jump ahead:
GraphQL is a flexible and efficient way to interact with APIs, allowing clients to specify exactly what data they need and receive responses with the requested information. GraphQL can be used from both the perspective of a client, such as a frontend web application, or a server.
To retrieve data in GraphQL, queries are used. For example, the following query could be used to request a list of to-do items from a server:
query TodoList { todoList { id title description } }
The query above would return a list of todo
item objects with id
, title
, description
, and date
fields.
In addition to retrieving data, GraphQL also supports mutations to make changes to the server. A simple example of a mutation to delete a specific to-do item from a database might look like this:
mutation deleteTodo($id: ID!) { deleteTodo(id: $id) { id } }
This mutation takes in the id
of the to-do item to be deleted and returns the id
of the deleted item when successful.
Apollo Client, built by the Apollo GraphQL team, is a toolkit designed to make it easy for a client application to communicate with a GraphQL API. The React Apollo library offers a set of specific tools that can be integrated into React components.
useQuery()
One of the main functions provided by React Apollo to execute GraphQL queries is the useQuery()
Hook. This Hook takes a GraphQL document as its first argument and returns a result
object that includes the data
, loading
, and error
statuses of the query request.
For example, consider the following TodoList
component that uses the useQuery()
Hook to request data from the TodoList
query example we shared above:
import * as React from 'react'; import { useQuery } from "@apollo/react-hooks"; const TODO_LIST = ` query TodoList { todoList { id title description } } `; export const TodoList = () => { const { loading, data, error } = useQuery(TODO_LIST); if (loading) { return <h2>Loading...</h2> } if (error) { return <h2>Uh oh. Something went wrong...</h2> } const todoItems = data.map((todo) => { return <li>{todo.title}</li> }) return ( <ul>{todoItems}</ul> ) }
In this example, the component displays a loading message while the query request is in progress, an error message if the request fails, and a list of to-do titles if and when the request is successful.
Attempting to render the above component in a unit test would likely result in failure due to a lack of context for the query or its results (loading
, data
, error
, etc.). Because of this, we can use the @apollo/react-testing library to mock the GraphQL request. Mocking the request allows us to properly test the component without relying on a live connection to the API.
MockedProvider
The @apollo/react-testing
library includes a utility called MockedProvider
that allows us to create a mock version of the ApolloProvider
component for testing purposes. The ApolloProvider
component is a top-level component that wraps our React app and provides the Apollo client as context throughout the app.
MockedProvider
enables us to specify the exact responses we want from our GraphQL requests in our tests, allowing us to mock these requests without actually making network requests to the API.
Let’s see how we can mock GraphQL requests in the loading, error, and success states.
To mock GraphQL requests in the loading state, we can wrap our components with MockedProvider
and provide an empty array as the value of the mocks
prop:
import * as React from 'react'; import { render } from "@testing-library/react"; import { TodoList } from '../TodoList'; describe("<TodoList />", () => { it("renders the expected loading message when the query is loading", async () => { const { /* get query helpers*/ } = render( <MockedProvider mocks={[]}> <TodoList></TodoList> </MockedProvider> ); // assertions to test component under loading state }); });
Assuming we’re using Jest as the unit testing framework and react-testing-library as the testing utility, we can assert that the component renders the expected text in its markup:
import * as React from 'react'; import { render } from "@testing-library/react"; import { TodoList } from '../TodoList'; describe("<TodoList />", () => { it("renders the expected loading message when the query is loading", async () => { const { queryByText } = render( <MockedProvider mocks={[]}> <TodoList /> </MockedProvider> ); // assert the loading message is shown expect(queryByText('Loading...')).toBeVisible(); }); });
By wrapping our components with MockedProvider
and providing mock request
and error
(or errors
) property values, we can simulate the error state by specifying the exact error responses we want for our GraphQL request:
import * as React from 'react'; import { render } from "@testing-library/react"; import { TodoList } from '../TodoList'; const TODO_LIST = ` query TodoList { todoList { id title description } } `; describe("<TodoList />", () => { // ... it('renders the expected error state', async () => { const todoListMock = { request: { query: TODO_LIST, }, error: new Error('Network Error!'), }; const { /* queries */ } = render( <MockedProvider mocks={[todoListMock]}> <TodoList></TodoList> </MockedProvider>, ); // assertions to test component under error state }); });
We’ll have our unit test assert that the <TodoList />
component correctly displays the expected error message when the TodoList
GraphQL query has failed with a network error:
import * as React from 'react'; import { render } from "@testing-library/react"; import { TodoList } from '../TodoList'; const TODO_LIST = ` query TodoList { todoList { id title description } } `; describe("<TodoList />", () => { // ... it('renders the expected error state', async () => { const todoListMock = { request: { query: TODO_LIST, }, error: new Error('Network Error!'), }; const { queryByText } = render( <MockedProvider mocks={[todoListMock]}> <TodoList></TodoList> </MockedProvider>, ); // assert the error message is shown expect(queryByText('Uh oh. Something went wrong...')).toBeVisible(); }); });
Lastly, to test GraphQL requests in a successful state, we can use the MockedProvider
utility component to wrap our components and provide mock values for the request
and result
properties in the mock GraphQL object. The result
property is used to simulate the expected successful outcome of the GraphQL request in our test:
import * as React from 'react'; import { render } from "@testing-library/react"; import { TodoList } from '../TodoList'; const TODO_LIST = ` query TodoList { todoList { id title description } } `; describe("<TodoList />", () => { // ... // ... it('renders the expected UI when data is available', async () => { const todoListMock = { request: { query: TODO_LIST, }, result: { data: { todos: [ { id: '1', title: 'Todo Item #1', description: 'Description for Todo Item #1', }, { id: '2', title: 'Todo Item #2', description: 'Description for Todo Item #2', } ] }, }, }; const { /* queries */ } = render( <MockedProvider mocks={[todoListMock]}> <TodoList></TodoList> </MockedProvider>, ); // assertions // ... }); });
GraphQL API requests are asynchronous, meaning that we often need to specify a waiting period in our tests before making assertions about the request’s outcome. React Testing Library provides the waitFor
utility to handle this scenario.
waitFor
can be used in a unit test when mocking an API call and waiting for the mock promises to resolve. An example of using waitFor
in the above unit test would be:
import * as React from 'react'; import { render, waitFor } from "@testing-library/react"; import { TodoList } from '../TodoList'; const TODO_LIST = ` query TodoList { todoList { id title description } } `; describe("<TodoList />", () => { // ... // ... it('renders the expected UI when data is available', async () => { const todoListMock = { request: { query: TODO_LIST, }, result: { data: { todos: [ { id: '1', title: 'Todo Item #1', description: 'Description for Todo Item #1', }, { id: '2', title: 'Todo Item #2', description: 'Description for Todo Item #2', } ] }, }, }; const { queryByText } = render( <MockedProvider mocks={[todoListMock]}> <TodoList></TodoList> </MockedProvider>, ); // use the waitFor utility to wait for API request to resolve await waitFor(() => { // assert the title for each todo item is visible expect(queryByText('Todo Item #1')).toBeVisible(); expect(queryByText('Todo Item #2')).toBeVisible(); }); }); });
Unit testing is a crucial aspect of software development. It verifies that our code is doing what we expect it to do and ensures that it works correctly with other code it interacts with.
When testing React components that communicate with an API, it’s important to avoid making actual API requests in our unit tests to save time and resources. Mocking GraphQL API requests, as described in this article, allows us to efficiently test our React components’ behavior without the added overhead of actual API requests.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.