REST и GraphQL endpoints

Тестирование компонентов, работающих с внешними API, является важной частью разработки на React. React Testing Library (RTL) сама по себе фокусируется на тестировании UI через поведение пользователя, поэтому прямой работы с сетевыми запросами она не выполняет. Для проверки взаимодействия с REST и GraphQL endpoints используются методы мокирования сетевых запросов и асинхронного ожидания результатов.


Мокирование REST API

Для тестирования компонентов, использующих REST API, чаще всего применяются библиотеки jest и msw (Mock Service Worker). Они позволяют перехватывать HTTP-запросы и возвращать заранее определённые ответы, без необходимости реально обращаться к серверу.

Пример мокирования с msw:

// handlers.js
import { rest } from 'msw';

export const handlers = [
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' }
      ])
    );
  }),
];
// test setup
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Компонент, который запрашивает пользователей:

import { useEffect, useState } from 'react';

function UsersList() {
  const [users, setUsers] = useState([]);
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers);
  }, []);
  
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

export default UsersList;

Тест с RTL и msw:

import { render, screen, waitFor } from '@testing-library/react';
import UsersList from './UsersList';

test('отображает список пользователей', async () => {
  render(<UsersList />);
  
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('Bob')).toBeInTheDocument();
  });
});

Ключевые моменты:

  • waitFor используется для ожидания асинхронных обновлений DOM.
  • Мокирование предотвращает зависимость тестов от внешнего API.
  • Можно изменять поведение сервера на лету через server.use для проверки ошибок.

Мокирование GraphQL API

GraphQL требует немного другой стратегии, так как запросы идут через единый endpoint и отличаются только структурой запроса. Для этого удобен пакет msw с graphql обработчиком.

Пример обработки GraphQL запроса:

import { graphql } from 'msw';

export const handlers = [
  graphql.query('GetUsers', (req, res, ctx) => {
    return res(
      ctx.data({
        users: [
          { id: '1', name: 'Alice' },
          { id: '2', name: 'Bob' }
        ]
      })
    );
  }),
];

Компонент с GraphQL (используется graphql-request):

import { useEffect, useState } from 'react';
import { request, gql } from 'graphql-request';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
    }
  }
`;

function UsersListGraphQL() {
  const [users, setUsers] = useState([]);
  useEffect(() => {
    request('/graphql', GET_USERS).then(data => setUsers(data.users));
  }, []);

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

export default UsersListGraphQL;

Тестирование с RTL:

import { render, screen, waitFor } from '@testing-library/react';
import UsersListGraphQL from './UsersListGraphQL';

test('GraphQL: отображает список пользователей', async () => {
  render(<UsersListGraphQL />);
  
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('Bob')).toBeInTheDocument();
  });
});

Особенности тестирования GraphQL:

  • Мокирование через graphql.query или graphql.mutation.
  • Можно проверять обработку ошибок сервера через ctx.errors.
  • Для сложных сценариев удобно изменять данные в моках прямо внутри теста.

Асинхронность и ожидание

В обоих случаях необходимо учитывать асинхронность запросов. React Testing Library предоставляет утилиты:

  • waitFor — ожидает выполнения колбэка до тех пор, пока не завершится асинхронное обновление DOM.
  • findBy* — комбинирует поиск и ожидание элемента в DOM.
  • waitForElementToBeRemoved — полезно для проверки загрузочных состояний.

Пример проверки загрузки:

test('отображает индикатор загрузки и список пользователей', async () => {
  render(<UsersList />);

  expect(screen.getByText('Загрузка...')).toBeInTheDocument();

  const alice = await screen.findByText('Alice');
  expect(alice).toBeInTheDocument();
});

Обработка ошибок и тестирование edge cases

Тестирование API должно охватывать не только успешные ответы, но и ошибки:

server.use(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.status(500));
  })
);

test('отображает сообщение об ошибке при сбое запроса', async () => {
  render(<UsersList />);
  
  const errorMessage = await screen.findByText('Ошибка загрузки пользователей');
  expect(errorMessage).toBeInTheDocument();
});

Для GraphQL ошибки обрабатываются аналогично:

server.use(
  graphql.query('GetUsers', (req, res, ctx) => {
    return res(ctx.errors([{ message: 'Internal server error' }]));
  })
);

Практические советы

  • Изолированность тестов: Каждый тест должен запускаться с чистым состоянием сервера (server.resetHandlers()).
  • Тестирование пользовательского поведения: Проверять результат в DOM, а не внутренние вызовы fetch.
  • Использование msw вместо jest mocks: Позволяет тестировать реальное взаимодействие с сетевым слоем.
  • Комбинация findBy и waitFor упрощает работу с асинхронными компонентами.

Эти методы создают надёжную стратегию тестирования компонентов React, которые взаимодействуют как с REST, так и с GraphQL endpoints, обеспечивая стабильность и предсказуемость тестов без обращения к реальному серверу.