Using React Testing Library to Test Your Next.js Application with TypeScript and GraphQL
Published: 2024-05-31 17:20:10

## Introduction

In the fast-paced web development world, ensuring your applications' quality and reliability is paramount. Testing is a critical component in the development process, especially for the frontend where user interaction is most prominent. Using modern tools like React, Next.js, TypeScript, GraphQL, and React Testing Library, developers can build robust and maintainable applications. In this blog post, we'll explore the importance of frontend testing and guide you through a practical example to get you started.
## Why Frontend Testing Matters
### Ensuring Code Quality

Frontend testing helps maintain high code quality by catching bugs early in the development process. It ensures that individual components function as expected, reducing the chances of errors in the production environment.
### Enhancing User Experience

By testing user interactions and UI components, developers can ensure a seamless and bug-free user experience. This is crucial for retaining users and providing a smooth navigation experience.
### Facilitating Refactoring

With a comprehensive suite of tests, developers can confidently refactor code without fear of breaking existing functionality. This encourages cleaner and more maintainable codebases.
### Improving Collaboration

Testing promotes better collaboration among team members. Clear and concise tests serve as documentation for how components should behave, making it easier for new developers to understand the codebase.
### Increasing Productivity

Automated tests save time in the long run by reducing the need for manual testing. This allows developers to focus on building new features and improving existing ones.
## Practical Example: Setting Up Frontend Testing

Let's dive into a practical example to illustrate how to set up and run frontend tests in a Next.js application with TypeScript, GraphQL, and React Testing Library.
### Step 1: Initialize the Repo

First, we'll create a new Next.js application using a predefined example with Jest for testing.

```bash
npx create-next-app@latest --example with-jest blog-todo-graphql --typescript
```


### Step 2: Install Apollo Client and GraphQL

Next, we'll install the Apollo Client and GraphQL to handle our GraphQL queries and mutations.

```bash
npm install @apollo/client graphql
```


### Step 3: Update `tsconfig.json`

To ensure that our imports are correctly resolved, update the `tsconfig.json` file to include the following `paths` configuration:

```json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}
```


### Step 4: Update React Testing Library

Ensure that you have the latest version of React Testing Library for the best features and bug fixes.

```bash
npm install @testing-library/react@15.0.6
```


### Step 5: Clean Up the Project Structure

We'll clean up the project structure by deleting unnecessary files and folders. Keep only the `app` folder with `page.tsx` and `layout.tsx` files.

```bash
rm -rf pages __tests__
```


### Step 6: Adjust `page.tsx` File

Update the `page.tsx` file to include the following content:

**app/page.tsx** 

```typescript
export const metadata = {
  title: "App Router",
};

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 7: 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 (
    <ul>
      {data.todos.map(
        (todo: { id: string; title: string; description: string }) => (
          <li key={todo.id}>
            <h3>{todo.title}</h3>
            <p>{todo.description}</p>
          </li>
        )
      )}
    </ul>
  );
};

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/add-todo.test.tsx** 

```typescript
import { render, screen, fireEvent } from "@testing-library/react";
import ApolloMockProvider from "@/mocks/apollo-mock-provider";
import { ADD_TODO } from "@/components/add-todo";
import { GET_TODOS } from "@/components/list-todos";
import Home from "app/page";

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: [],
      },
    },
  },
  {
    request: {
      query: GET_TODOS,
    },
    result: {
      data: {
        todos: [
          {
            id: 1,
            title: "New Test Todo",
            description: "New Test Description",
          },
        ],
      },
    },
  },
];

test("adds a todo", async () => {
  render(
    <ApolloMockProvider mocks={mocks}>
      <Home />
    </ApolloMockProvider>
  );

  // Check that initially there are no todos
  expect(screen.queryByText("New Test Todo")).not.toBeInTheDocument();
  expect(screen.queryByText("New Test Description")).not.toBeInTheDocument();

  // Add a new todo
  fireEvent.change(screen.getByPlaceholderText(/title/i), {
    target: { value: "New Test Todo" },
  });
  fireEvent.change(screen.getByPlaceholderText(/description/i), {
    target: { value: "New Test Description" },
  });

  fireEvent.click(screen.getByText("Add Todo"));

  // Check that the new todo is added
  expect(await screen.findByText("New Test Todo")).toBeInTheDocument();
  expect(await screen.findByText("New Test Description")).toBeInTheDocument();
});
```



This test checks the functionality of adding a new todo. It uses the mocked Apollo Provider to simulate the GraphQL requests and responses.

**tests/list-todos.test.tsx** 

```typescript
import { render, screen } from "@testing-library/react";
import ListTodos from "@/components/list-todos";
import ApolloMockProvider from "@/mocks/apollo-mock-provider";
import { GET_TODOS } from "@/components/list-todos";

const mocks = [
  {
    request: {
      query: GET_TODOS,
    },
    result: {
      data: {
        todos: [
          { id: 1, title: "Test Todo 1", description: "Test Description 1" },
          { id: 2, title: "Test Todo 2", description: "Test Description 2" },
        ],
      },
    },
  },
];

test("lists all todos", async () => {
  render(
    <ApolloMockProvider mocks={mocks}>
      <ListTodos />
    </ApolloMockProvider>
  );

  expect(screen.getByText("Loading...")).toBeInTheDocument();

  expect(await screen.findByText("Test Todo 1")).toBeInTheDocument();
  expect(await screen.findByText("Test Description 1")).toBeInTheDocument();

  expect(await screen.findByText("Test Todo 2")).toBeInTheDocument();
  expect(await screen.findByText("Test Description 2")).toBeInTheDocument();
});
```



This test checks that the list of todos is correctly fetched and displayed. It uses the mocked Apollo Provider to supply the test data.
### 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.

To run the tests, use the command below

```bash
npm run test
```


## 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. By integrating testing into your workflow, you can build robust applications with confidence. Follow the steps and add the provided files to set up a solid testing environment for your Next.js application. Happy testing!