Introduction
In my previous blog post, I discussed using React Testing Library to test React applications. Recently, Cypress introduced a new way to test components without requiring a full end-to-end solution. I decided to give it a try and was pleasantly surprised by its effectiveness. I seamlessly integrated it into my current project, which has a robust codebase. This type of testing is ideal for common scenarios, like the one I described in my last post, and I'll demonstrate it again here using Cypress. I've already covered the importance of testing in my previous blog, so let's dive straight into the code.
Step 1: Initialize the Repo
First, we'll create a new Next.js application. Currently, Cypress doesn't support Component Testing with server components or Next.js versions above 14. For more details, refer to the Next.js documentation on testing with Cypress.
I'm using Next.js to simplify the setup but feel free to user other solutions. The command below will generate a new application with Typescript.
npx create-next-app@latest blog-todo-graphql --typescript
Step 2: Install Apollo Client and GraphQL
Next, we'll install Apollo Client and GraphQL to handle our GraphQL queries and mutations. Also, don't forget the cypress library.
npm install @apollo/client graphql cypress
Step 3: Clean Up the Project Structure
We'll clean up the project structure by deleting the api
folder.
Step 4: Adjust index.tsx
File
Update the index.tsx
file to include the following content:
pages/index.tsx
import AddTodo from "@/components/add-todo";
import ListTodos from "@/components/list-todos";
const Home = () => {
return (
<div>
<h1>Todo List</h1>
<AddTodo />
<ListTodos />
</div>
);
};
export default Home;
Step 5: Add the Necessary Project Files
Here are the files we need to add to our project, along with a brief explanation of their purpose:
schemas/index.gql
type Todo {
id: String!
title: String!
description: String!
}
type Query {
todos: [Todo!]!
}
type Mutation {
addTodo(title: String!, description: String!): Todo!
}
This file defines the GraphQL schema for our Todo application, including the Todo type, a query to fetch todos, and a mutation to add a new todo.
components/add-todo.tsx
import { useState } from "react";
import { useMutation, gql } from "@apollo/client";
import { GET_TODOS } from "@/components/list-todos";
export const ADD_TODO = gql`
mutation AddTodo($title: String!, $description: String!) {
addTodo(title: $title, description: $description) {
id
title
description
}
}
`;
const AddTodo = () => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [addTodo] = useMutation(ADD_TODO);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await addTodo({
variables: { title, description },
refetchQueries: [{ query: GET_TODOS }],
});
setTitle("");
setDescription("");
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
required
/>
<input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description"
required
/>
<button type="submit">Add Todo</button>
</form>
);
};
export default AddTodo;
This component provides a form for adding new todos. It uses the Apollo Client to send a GraphQL mutation to add the todo and refetches the list of todos after adding a new one.
components/list-todos.tsx
import { useQuery, gql } from "@apollo/client";
export const GET_TODOS = gql`
query GetTodos {
todos {
id
title
description
}
}
`;
const ListTodos = () => {
const { loading, error, data } = useQuery(GET_TODOS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return data.todos.map(
(todo: { id: string; title: string; description: string }) => (
<div key={todo.id}>
<h3>{todo.title}</h3>
<p>{todo.description}</p>
</div>
)
);
};
export default ListTodos;
This component fetches and displays a list of todos using the Apollo Client. It shows loading and error states while the data is being fetched.
mocks/apollo-mock-provider.tsx
import React from "react";
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
interface ApolloMockProviderProps {
mocks: MockedResponse[];
children: React.ReactNode;
}
const ApolloMockProvider: React.FC<ApolloMockProviderProps> = ({ mocks, children }) => (
<MockedProvider mocks={mocks} addTypename={false}>
{children}
</MockedProvider>
);
export default ApolloMockProvider;
This component provides a mocked Apollo Client for testing purposes. It uses the MockedProvider from @apollo/client/testing
to supply mock data for our tests.
tests/home.cy.tsx
import React from "react";
import Home from "../pages/index";
import { GET_TODOS } from "@/components/list-todos";
import { ADD_TODO } from "@/components/add-todo";
import ApolloMockProvider from "@/mocks/apollo-mock-provider";
const mocks = [
{
request: {
query: ADD_TODO,
variables: {
title: "New Test Todo",
description: "New Test Description",
},
},
result: {
data: {
addTodo: {
id: 1,
title: "New Test Todo",
description: "New Test Description",
},
},
},
},
{
request: {
query: GET_TODOS,
},
result: {
data: {
todos: [
{
id: 1,
title: "New Test Todo",
description: "New Test Description",
},
],
},
},
},
];
describe("<Home />", () => {
it("renders", () => {
cy.mount(
<ApolloMockProvider mocks={mocks}>
<Home />
</ApolloMockProvider>
);
cy.get('[placeholder="Title"]').type("New Test Todo");
cy.get('[placeholder="Description"]').type("New Test Description");
cy.get("button").click();
cy.get("h3").should("contain", "New Test Todo");
cy.get("p").should("contain", "New Test Description");
});
});
This is our first Cypress test case. Similar to my approach with React Testing Library, this test verifies the functionality of adding a new todo to the list. It uses a mocked Apollo Provider to simulate GraphQL requests and responses.
In this example, I tested both <AddTodo />
and <ListTodos />
components through the Home page component. However, in some cases, it might be more practical to test components in isolation. For instance, if the Home page were a dashboard screen filled with various components, you would need to mock everything.
In such scenarios, testing each component individually is more effective, as the primary goal of these tests is to provide confidence during development. You'll quickly see that Cypress component testing mode makes this process straightforward and efficient.
Running Your Test
To open the Cypress Component Testing GUI, use the following command:
npx cypress open --component
You will need to configure a few settings, but the process is quick. Follow the instructions until you reach the screen shown below:
Pick your browser and select the home.cy.tsx
test we created. It should display the following screen:
And there it is! Try creating tests for more complex cases that you encounter. For example, what if you have a complex form with two or more fields that depend on another field? These tests will provide quick feedback if you need to refactor this module at some point. Trust me, you might need to do it.
Benefits of Independent Tests
As you can see, in this setup, we didn't need to add the ApolloProvider
component to our main application, just the MockedProvider
for our tests. It's important to note that the ApolloProvider
and MockedProvider
serve different purposes and are independent of each other. Since this content focuses on demonstrating the tests and not the application running, we don't need to add the ApolloProvider
here. However, in a real application, you must include the ApolloProvider
to integrate your application with the actual API.
Conclusion
Frontend testing is an indispensable part of modern web development. It ensures that your application is reliable, maintainable, and provides a great user experience. If you read my previous article and tried using React Testing Library, you will find that Cypress Component Testing is much easier for more complex scenarios. Why not give this new approach a try and share your experiences with me? I’m eager to hear about your results. Happy testing!