Using Cypress Component Testing in your Next.js Application with TypeScript and GraphQL
Published: 2024-06-19 21:00:03

## 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](https://nextjs.org/docs/pages/building-your-application/testing/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.

```bash
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.

```bash
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** 

```typescript
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** 

```graphql
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** 

```typescript
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** 

```typescript
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** 

```typescript
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** 

```typescript
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:


```bash
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:
![Cypress Initial Setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ytl0mrndlhf1nkbhfqzr.png) 
Pick your browser and select the `home.cy.tsx` test we created. It should display the following screen:![Cypress home page test case](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8o5qthwwbnrx356fdrsw.png) 

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!