Введение

Тестирование — неотъемлемая часть процесса разработки программного обеспечения, и React — не исключение. В мире React Jest и библиотека тестирования React стали незаменимыми инструментами для создания надежных, удобных в сопровождении и масштабируемых тестов для ваших приложений. В этой статье мы рассмотрим основы тестирования компонентов React с помощью Jest и библиотеки тестирования React, а также предоставим вам знания и лучшие практики, необходимые для обеспечения надежности ваших приложений и отсутствия ошибок.

  1. Настройка среды тестирования

Прежде чем погрузиться в процесс тестирования, нам нужно настроить нашу тестовую среду. Для этого мы установим Jest, React Testing Library и все необходимые зависимости:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom

Затем создайте файл jest.config.js в корневом каталоге вашего проекта для настройки Jest:

module.exports = {
  testEnvironment: 'jsdom',
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
  setupFilesAfterEnv: ['./jest.setup.js'],
};

Создайте файл jest.setup.js в корневом каталоге вашего проекта, чтобы импортировать необходимые утилиты тестирования:

import '@testing-library/jest-dom';

Теперь ваша тестовая среда настроена и готова к работе!

2. Написание первого теста

Начнем с создания простого компонента React для тестирования. Создайте файл Counter.js:

import React, { useState } from 'react';

export const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
};

Теперь создайте файл Counter.test.js для написания наших тестов:

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

test('renders the counter component with initial state', () => {
  const { getByText } = render(<Counter />);
  const counterText = getByText(/Counter: 0/i);
  expect(counterText).toBeInTheDocument();
});

test('increments the counter value on clicking the increment button', () => {
  const { getByText } = render(<Counter />);
  const incrementButton = getByText(/Increment/i);
  fireEvent.click(incrementButton);
  const counterText = getByText(/Counter: 1/i);
  expect(counterText).toBeInTheDocument();
});

test('decrements the counter value on clicking the decrement button', () => {
  const { getByText } = render(<Counter />);
  const decrementButton = getByText(/Decrement/i);
  fireEvent.click(decrementButton);
  const counterText = getByText(/Counter: -1/i);
  expect(counterText).toBeInTheDocument();
});

В этом примере мы используем render для рендеринга нашего компонента Counter и fireEvent для имитации взаимодействия с пользователем. Затем мы используем запрос getByText для поиска элементов в DOM и делаем утверждения, используя функцию Jest expect.

3. Рекомендации

  • Всегда тестируйте взаимодействия с пользователем, а не детали реализации.
  • Используйте асинхронные утилиты, такие как запросы waitFor и findBy*, для компонентов с асинхронным поведением.
  • Используйте пользовательские функции рендеринга, чтобы обернуть компоненты поставщиками контекста или поставщиками темы.
  • Используйте screen для запроса элементов вместо методов деструктурирования из render.

4. Тестирование асинхронного поведения

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

import React, { useState, useEffect } from 'react';

export const User = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        setError(error.message);
      }
    };

    fetchUser();
  }, [userId]);

  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
};

Теперь давайте напишем тест для этого компонента с помощью асинхронных утилит. Создайте файл User.test.js:

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

// Mock the API call
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () =>
      Promise.resolve({
        id: 1,
        name: 'John Doe',
        email: '[email protected]',
      }),
  }),
);

test('fetches and displays user data', async () => {
  render(<User userId={1} />);

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

  await waitFor(() => screen.getByText(/john doe/i));

  expect(screen.getByText(/john doe/i)).toBeInTheDocument();
  expect(screen.getByText(/john\.doe@example\.com/i)).toBeInTheDocument();

  // Ensure the fetch function was called with the correct URL
  expect(fetch).toHaveBeenCalledWith('https://jsonplaceholder.typicode.com/users/1');
});

В этом примере мы имитируем функцию fetch для имитации вызова API. Мы используем waitFor, чтобы дождаться появления элемента в DOM, прежде чем делать утверждения.

5. Пользовательские функции рендеринга

В некоторых случаях вам может понадобиться обернуть ваши компоненты поставщиками контекста или поставщиками темы. Вы можете создать пользовательскую функцию рендеринга для обработки этих сценариев. Создайте файл test-utils.js:

import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';

const customRender = (ui, options) =>
  render(ui, { wrapper: ({ children }) => <ThemeProvider theme={theme}>{children}</ThemeProvider>, ...options });

export * from '@testing-library/react';
export { customRender as render };

Теперь вы можете использовать пользовательскую функцию рендеринга в своих тестах:

import { render, screen } from './test-utils';
import { MyComponent } from './MyComponent';

test('renders the MyComponent', () => {
  render(<MyComponent />);
  // Your assertions here
});

6. Использование объекта screen

Вместо деструктуризации методов запроса из функции render рекомендуется использовать объект screen для запроса элементов. Это приводит к более чистым и читаемым тестам:

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

test('increments the counter value on clicking the increment button', () => {
  render(<Counter />);
  const incrementButton = screen.getByText(/Increment/i);
  fireEvent.click(incrementButton);
  const counterText = screen.getByText(/Counter: 1/i);
  expect(counterText).toBeInTheDocument();
});

Используя объект screen, вы можете легко увидеть, какие запросы используются в тесте, что упрощает понимание и поддержку.

7. Тестирование пользовательских хуков

Для тестирования пользовательских хуков вы можете использовать пакет `@testing-library/react-hooks`. Сначала установите пакет:

npm install --save-dev @testing-library/react-hooks

Давайте создадим собственный хук с именем useCounter:

import { useState } from 'react';

export const useCounter = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return { count, increment, decrement };
};

Теперь создайте файл useCounter.test.js для проверки нашего пользовательского хука:

import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';

test('should increment and decrement the counter value', () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);

  act(() => {
    result.current.decrement();
  });

  expect(result.current.count).toBe(0);
});

В этом примере мы используем renderHook для рендеринга нашего пользовательского хука и act для выполнения действий с хуком. Затем мы делаем утверждения, используя функцию Jest expect.

Заключение

Освоение тестирования React с помощью Jest и библиотеки тестирования React необходимо для обеспечения надежности, удобства обслуживания и масштабируемости ваших приложений. Следуя рекомендациям и методам, изложенным в этой статье, вы будете на пути к созданию надежного набора тестов для своих компонентов и хуков React. Продолжайте учиться, тестировать и создавать надежные приложения!

Подробнее