Mocking GraphQL resolvers can drastically speed up frontend development by allowing you to build and test UI components without a live backend.
Here’s how you can set up and use mock resolvers with Apollo Client, focusing on a practical example.
Let’s say you have a GraphQL schema that looks like this:
type User {
id: ID!
name: String!
email: String
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
author: User!
}
type Query {
user(id: ID!): User
posts: [Post!]!
}
And your frontend needs to display a user’s profile, including their posts.
Without a backend, you can use Apollo Client’s MockedProvider to simulate responses.
First, install the necessary dependencies:
npm install @apollo/client graphql
# or
yarn add @apollo/client graphql
Now, let’s create a mock for the user query. You’ll define a MockedResponse object that maps a specific query and variables to a mocked data payload.
import { MockedProvider } from '@apollo/client/testing';
import { gql } from '@apollo/client';
const GET_USER_QUERY = gql`
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
posts {
id
title
}
}
}
`;
const mocks = [
{
request: {
query: GET_USER_QUERY,
variables: { userId: '1' },
},
result: {
data: {
user: {
id: '1',
name: 'Alice Wonderland',
email: 'alice@example.com',
posts: [
{ id: 'p1', title: 'My First Post' },
{ id: 'p2', title: 'Adventures in Mocking' },
],
},
},
},
},
];
You then wrap your component or test with MockedProvider, passing in the mocks array.
import React from 'react';
import { useQuery } from '@apollo/client';
function UserProfile({ userId }) {
const { loading, error, data } = useQuery(GET_USER_QUERY, {
variables: { userId },
});
if (loading) return <p>Loading profile...</p>;
if (error) return <p>Error loading profile: {error.message}</p>;
const user = data?.user;
return (
<div>
<h2>{user?.name}</h2>
<p>Email: {user?.email}</p>
<h3>Posts:</h3>
<ul>
{user?.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
// In your test file or app root:
function App() {
return (
<MockedProvider mocks={mocks} addTypename={false}>
<UserProfile userId="1" />
</MockedProvider>
);
}
The addTypename={false} option is often useful during development and testing with mocks. When true, Apollo Client expects __typename fields in your mock data to match your schema. Setting it to false simplifies mock creation, as you don’t need to manually add these fields.
You can also mock network errors. To do this, instead of a result field, you provide an error field.
const errorMocks = [
{
request: {
query: GET_USER_QUERY,
variables: { userId: '2' },
},
error: new Error('Failed to fetch user data'),
},
];
And render your component within a MockedProvider using these error mocks:
function AppWithError() {
return (
<MockedProvider mocks={errorMocks} addTypename={false}>
<UserProfile userId="2" />
</MockedProvider>
);
}
This setup allows you to test both successful data fetching and error states independently of your backend. For mutations, the structure is similar, using MockedResponse with request.query and result.data or error.
When Apollo Client encounters a query or mutation, it checks if there’s a matching entry in the mocks array based on the query document and variables. If a match is found, it returns the specified result or error immediately, bypassing the actual network request. This makes your frontend code run much faster during development cycles.
The truly powerful aspect of MockedProvider is its ability to simulate complex scenarios. You can have multiple mocks for the same query but with different variables, and MockedProvider will pick the one that matches the variables passed to useQuery. This allows for granular testing of different user states or data configurations without needing multiple backend environments.
The MockedProvider also keeps track of executed requests. If a query is executed that doesn’t have a corresponding mock, it will throw an error (unless you configure it otherwise), which is excellent for ensuring your frontend is only requesting data that has been explicitly mocked for testing.
If you have a query that fetches a list of items, and you want to simulate different list lengths or content for those items, you can define multiple mocks for the same query, each with a distinct set of variables.
For example, if you had a GET_POSTS_QUERY:
const GET_POSTS_QUERY = gql`
query GetPosts($limit: Int) {
posts(limit: $limit) {
id
title
}
}
`;
const postMocks = [
{
request: {
query: GET_POSTS_QUERY,
variables: { limit: 5 },
},
result: {
data: {
posts: [
{ id: 'p1', title: 'Post One' },
{ id: 'p2', title: 'Post Two' },
{ id: 'p3', title: 'Post Three' },
{ id: 'p4', title: 'Post Four' },
{ id: 'p5', title: 'Post Five' },
],
},
},
},
{
request: {
query: GET_POSTS_QUERY,
variables: { limit: 2 },
},
result: {
data: {
posts: [
{ id: 'p1', title: 'Post One' },
{ id: 'p2', title: 'Post Two' },
],
},
},
},
];
When your component calls useQuery(GET_POSTS_QUERY, { variables: { limit: 5 } }), the first mock will be used. If it calls useQuery(GET_POSTS_QUERY, { variables: { limit: 2 } }), the second mock will be used. This is crucial for testing pagination or list-loading components.
The next logical step after mastering mock resolvers is understanding how to integrate this with a state management solution that might also rely on asynchronous data, or how to handle more complex mocking scenarios like delayed responses.