In this blog post, we'll explore the art of mocking Apollo Server Schemas. Adopting a GraphQL-First development strategy is a game-changer for front-end developers. It empowers you to forge ahead with creating UI components and features seamlessly, all without being held back by backend development timelines. Let's dive into how this approach can accelerate your development process and enhance your productivity. All code produced in this post can be found here: https://github.com/maxshugar/apollo-server-mocking-example
The Necessity of Mocking in Development
Mocking in software development, particularly in the context of GraphQL and Apollo Server, is not just a convenience; it’s often a necessity. Why? Because it directly addresses several pain points developers face, especially when working on the front end independent of backend progress.
Real-World Scenarios Where Mocking Shines:
Early Stage Development: Imagine you’re working on a new feature that relies on data from a service not yet implemented. Without mocking, your progress stalls. You’re stuck waiting for the backend team to catch up, which could delay the entire project. With mocking, you can simulate the backend, proceed with development, and refine the UI and user experience, all without any backend dependency.
Testing Edge Cases: Testing how your application handles edge cases or error conditions can be challenging without a fully controlled environment. For example, how does your app behave when it receives incomplete data or none at all? Setting up these scenarios with a real backend can be cumbersome and time-consuming. Mocking allows you to simulate these conditions easily, ensuring your app is robust and handles various data states gracefully.
Continuous Integration (CI) and Continuous Deployment (CD): CI/CD pipelines thrive on automation. Requiring a live backend for front-end tests can introduce flakiness and dependencies that slow down these processes. Mocks ensure that your automated tests are self-contained, leading to faster, more reliable builds.
Development and Testing in Parallel: When frontend and backend development occur simultaneously, the API contract might change, leading to integration issues. Mocking based on agreed contracts allows frontend development to proceed with a stable, predictable API, reducing integration headaches down the line.
Having outlined the critical role mocking plays in overcoming common development challenges, let’s now transition into the practical aspects of how you can implement this approach in your projects.
Step 0: Installing Dependencies
In this tutorial, we will be building a book management app using expo, although the same steps apply to a React project.
npm i @apollo/client @graphql-tools/mock @graphql-tools/schema
Step 1: Defining our Type Definitions
import { gql } from "@apollo/client";
const typeDefs = gql`
type Book {
title: String!
publishedAt: String!
}
input AddBookInput {
title: String!
publishedAt: String!
}
type Query {
getBooks: [Book]
}
type Mutation {
addBook(input: AddBookInput): ID!
}
`;
Let’s assume we’re working on a simple application that needs to manage a list of books. Our GraphQL schema includes operations for fetching books and creating a new book. With these type definitions, we can simulate real interactions with our GraphQL server without actually needing the server to be running.
Step 2: Defining our Mocks
import { MockList } from "@graphql-tools/mock";
const mocks = {
Book: () => ({
title: () => "Book Title",
publishedAt: () => "2021-01-01",
}),
Query: () => ({
getBooks: () => [
{ title: "Title 1", publishedAt: "2021-01-01" },
{ title: "Title 2", publishedAt: "2021-01-02" },
{ title: "Title 3", publishedAt: "2021-01-03" },
{ title: "Title 4", publishedAt: "2021-01-04" },
{ title: "Title 5", publishedAt: "2021-01-05" },
],
}),
Mutation: () => ({
addBook: () => "123",
}),
};
The mocks object defines how the data for different types in the GraphQL schema should be mocked. The getBooks query returns an array of type Book. The addBooks mutation mocks the return type ID which is serialised as a string. To enhance the realism and variability of the mocked data, we could integrate a faker library, although that is out of scope for this tutorial.
Step 3: Creating the Schema
import { addMocksToSchema } from "@graphql-tools/mock";
import { makeExecutableSchema } from "@graphql-tools/schema";
const schema = makeExecutableSchema({ typeDefs });
export const schemaWithMocks = addMocksToSchema({ schema, mocks });
We take our GraphQL type definitions (typeDefs) and use makeExecutableSchema to create an executable schema. We then use addMocksToSchema to add our mock data to our executable schema. This step involves taking the schema we created and injecting it with mock data based on the mocks object we defined earlier.
Step 4: Creating the Apollo Client
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { SchemaLink } from "@apollo/client/link/schema";
import { schemaWithMocks } from "./schema";
const API_URL = process.env.EXPO_PUBLIC_API_URL;
const httpLink = createHttpLink({ uri: API_URL });
const schemaLink = new SchemaLink({ schema: schemaWithMocks });
export const apolloClient = new ApolloClient({
link: API_URL ? httpLink : schemaLink,
cache: new InMemoryCache(),
});
The API_URL variable represents the endpoint of a live GraphQL API loaded in from an environment variable. This setup is particularly useful for development and testing, allowing us to switch seamlessly between a live backend and a mocked environment without changing the client-side code. The Apollo Link library helps you customize the flow of data between Apollo Client and your GraphQL server. If API_URL has been set, we use HttpLink which is a terminating link that sends a GraphQL operation to a remote endpoint over HTTP. If the API_URL has not been set, we make use of a SchemaLink, which allows you to perform GraphQL operations on a provided schema.
Step 5: Apollo Provider
import { ApolloProvider } from '@apollo/client';
import { AddBookButton } from './src/components/addBookButton';
import { BookList } from './src/components/bookList';
import { apolloClient } from './src/gql/client';
export default function App() {
return (
<ApolloProvider client={apolloClient}>
<BookList />
<AddBookButton />
</ApolloProvider>
);
}
Next, we must wrap our application in an ApolloProvider, which places Apollo Client in the context, enabling you to access it from anywhere in your component tree. In the example above I have defined two components which consume the query and mutation we defined earlier.
Step 6: Querying our Schema
import { useQuery } from "@apollo/client";
import { StyleSheet, Text, View } from "react-native";
import { gql } from "@apollo/client";
const GET_BOOKS_QUERY = gql`
query GetBooks {
getBooks {
title
publishedAt
}
}
`;
export const BookList = () => {
const { loading, error, data } = useQuery(GET_BOOKS_QUERY);
if (loading) return <Text>Loading...</Text>;
if (error) return <Text>Error: {error.message}</Text>;
return (
<View>
{data.getBooks.map((book: { title: string; publishedAt: string }, index: number) => (
<View key={index} style={styles.container} >
<Text>Title: {book.title}</Text>
<Text>Published At: {book.publishedAt}</Text>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'flex-start',
backgroundColor: '#d3d3d3',
borderColor: 'blue',
borderWidth: 2,
margin: 5
}
});
Our BookList component makes use of the useQuery hook to retrieve a list of books. Our schema link will intercept this request and generate a Book list using the mocks we defined earlier.
Step 7: Mutating our Schema
import { useMutation } from "@apollo/client";
import React from "react";
import { Text, TouchableOpacity } from "react-native";
import { gql } from "@apollo/client";
const ADD_BOOK_MUTATION = gql`
mutation AddBook($input: AddBookInput!) {
addBook(input: $input)
}
`;
export const AddBookButton = () => {
const [addBook, { data, loading, error }] = useMutation(ADD_BOOK_MUTATION);
console.log({ data, loading, error })
return (
<TouchableOpacity
style={{ padding: 10, backgroundColor: "lightblue", justifyContent: "center", alignItems: "center" }}
onPress={() => {
addBook({
variables: {
input: {
title: "New Book",
publishedAt: new Date().toISOString(),
}
},
});
}}
>
<Text>Add Book</Text>
</TouchableOpacity>
);
};
Our AddBookButton component consumes the ADD_BOOK_MUTATION. When the mutation is invoked, the response is printed to the console. When working with a live API, we may want to re-fetch the BookList query data, or return the whole book from the mutation and update the cache.
Step 8: Live Integration with an Apollo Server
When you are ready to begin integrating your app with your Apollo server, set your EXPO_PUBLIC_API_URL environment variable to the address of your GraphQL server. If you are developing a react application, your environment variable may look something like REACT_APP_API_URL. Just make sure to update the file where we define our Apollo client to make use of this variable.
Conclusion:
We’ve demystified the process of mocking Apollo Server Schemas and highlighted the agility it brings to front-end development. By adopting this strategy, developers can significantly accelerate their workflow, enhance productivity, and ensure that when the backend catches up, integrating with a live Apollo Server is seamless. Remember, the key to successful GraphQL-First development lies in understanding and utilizing these tools effectively to create dynamic, robust, and user-centric applications. Your feedback, questions, and experiences are invaluable, not just to me but to everyone reading this. Let’s keep the conversation going and continue learning together. Happy coding!
Top comments (0)