DEV Community

Marcos Schead
Marcos Schead

Posted on

Using Cypress Component Testing in your Next.js Application with TypeScript and GraphQL

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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");
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You will need to configure a few settings, but the process is quick. Follow the instructions until you reach the screen shown below:
Cypress Initial Setup
Pick your browser and select the home.cy.tsx test we created. It should display the following screen:Cypress home page test case

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!

Top comments (0)