Render props паттерн тестирование

Паттерн Render Props позволяет компонентам делиться функциональностью через функцию, которая возвращает элементы JSX. Это особенно полезно для создания гибких компонентов, которые могут принимать различные логики и состояния. Тестирование таких компонентов требует особого подхода, поскольку мы имеем дело с компонентами, которые не напрямую рендерят элементы, а передают функциональность через функцию. В этой статье будет рассмотрено, как тестировать компоненты, использующие паттерн Render Props, с помощью React Testing Library.

Особенности компонента с Render Props

Компонент, использующий Render Props, предоставляет функцию, которая передается в качестве дочернего элемента. Эта функция, в свою очередь, может использовать состояние и логику компонента и возвращать JSX. Пример:

const DataFetcher = ({ render }) => {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetch('/api/data')
      .then((response) => response.json())
      .then((data) => setData(data));
  }, []);

  return render(data);
};

Здесь компонент DataFetcher использует проп render, который принимает функцию для рендеринга данных. Это позволяет пользователю компонента гибко управлять выводом в зависимости от полученных данных.

Подход к тестированию компонента с Render Props

При тестировании компонента с Render Props важно понимать, что необходимо проверить не только сам процесс передачи данных через функцию, но и то, как этот компонент взаимодействует с состоянием, асинхронными операциями и пользовательскими событиями.

1. Проверка, что компонент рендерит корректно

Первым шагом является проверка, что компонент рендерит правильный JSX, используя переданную функцию. Для этого можно подать в render проп функцию, которая возвращает элемент, и убедиться, что он рендерится корректно.

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

test('DataFetcher renders data correctly', () => {
  const mockRender = (data) => <div>{data ? data : 'Loading...'}</div>;

  render(<DataFetcher render={mockRender} />);

  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

В данном тесте мы передаем компоненту DataFetcher функцию mockRender, которая отображает “Loading…” до того, как данные будут загружены.

2. Асинхронные данные

Так как компонент может работать с асинхронными запросами, важно протестировать его поведение при получении данных. Для этого можно использовать асинхронные ожидания, чтобы проверить, что компонент рендерит правильные данные после завершения асинхронного запроса.

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

test('DataFetcher loads and renders data', async () => {
  const mockData = { id: 1, name: 'Test' };
  const mockRender = (data) => <div>{data ? data.name : 'Loading...'}</div>;

  // Мокаем асинхронную загрузку данных
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve(mockData),
    })
  );

  render(<DataFetcher render={mockRender} />);

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

  // Ожидаем, что данные загрузятся и отобразятся
  await waitFor(() => expect(screen.getByText(mockData.name)).toBeInTheDocument());
});

В данном примере мы используем jest.fn() для мока асинхронного вызова fetch. Мы проверяем, что вначале отображается “Loading…”, а после завершения загрузки данных компонент рендерит правильное имя.

3. Проверка взаимодействий с данными

Если компонент с Render Props имеет внутреннюю логику для обновления данных, можно протестировать, как он реагирует на изменения состояния или взаимодействие с пользователем.

import { render, screen, fireEvent } from '@testing-library/react';

test('DataFetcher updates data on button click', async () => {
  const mockData1 = { id: 1, name: 'Test1' };
  const mockData2 = { id: 2, name: 'Test2' };
  const mockRender = (data, updateData) => (
    <div>
      <div>{data ? data.name : 'Loading...'}</div>
      <button onCl ick={() => updateData(mockData2)}>Change Data</button>
    </div>
  );

  const mockUpdateData = jest.fn();

  render(<DataFetcher render={(data) => mockRender(data, mockUpdateData)} />);

  fireEvent.click(screen.getByText('Change Data'));

  expect(mockUpdateData).toHaveBeenCalledWith(mockData2);
});

Здесь компонент рендерит кнопку, которая при нажатии обновляет данные с помощью функции updateData. Мы проверяем, что при клике вызывается функция обновления данных, и передаются правильные значения.

4. Использование user-event для эмуляции взаимодействий

Для более сложных взаимодействий, например, при вводе текста в поле, можно использовать библиотеку @testing-library/user-event, которая имитирует действия пользователя более реалистично, чем fireEvent.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('User input triggers render update', async () => {
  const mockRender = (data, updateData) => (
    <div>
      <input
        type="text"
        value={data ? data.name : ''}
        onCha nge={(e) => updateData({ name: e.target.value })}
      />
      <div>{data ? data.name : 'No Data'}</div>
    </div>
  );

  const mockData = { name: 'Initial' };
  const mockUpdateData = jest.fn();

  render(<DataFetcher render={(data) => mockRender(data, mockUpdateData)} />);

  userEvent.type(screen.getByRole('textbox'), 'Updated Name');

  expect(mockUpdateData).toHaveBeenCalledWith({ name: 'Updated Name' });
});

Этот тест проверяет, как компонент обновляет данные при вводе текста в поле ввода. Мы используем userEvent.type для имитации ввода пользователя.

Заключение

Тестирование компонентов с Render Props требует внимания к деталям, особенно когда речь идет об асинхронных данных и взаимодействиях. С использованием React Testing Library можно легко протестировать такие компоненты, проверяя, что они рендерят правильные данные, обрабатывают асинхронные запросы и корректно взаимодействуют с пользователем. Тесты должны покрывать основные случаи использования компонента, включая загрузку данных, обновление состояния и взаимодействия с пользователем.