Розробка Unit-тестів для компонентів (React Testing Library)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка Unit-тестів для компонентів (React Testing Library)
Середня
~3-5 робочих днів
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка Unit-тестів для компонентів (React Testing Library)

React Testing Library будується на одній ідеї: тесты повинні перевіряти поведінку компонента з точки зору користувача, а не деталі реалізації. Немає прямого доступу до state, немає перевірки instance-змінних, немає wrapper.find(MyInternalComponent). Тільки те, що реально рендерується в DOM та як на це реагує користувач.

Це і перевага, і обмеження. Тесты не ломаються при рефакторингу внутрішньої логіки, але вимагають правильно проектувати компоненти — з доступними ролями, зрозумілими data-testid та передбачуваною поведінкою.

Встановлення та конфігурація

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event vitest jsdom

Конфігурація Vitest з підтримкою jsdom:

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true,
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';

Базові патерни

Перше, що потрібно зрозуміти — різниця між getBy, queryBy та findBy:

  • getBy — синхронний, кидає якщо не знайдений
  • queryBy — синхронний, повертає null якщо не знайдений (для перевірки відсутності)
  • findBy — асинхронний, чекає появи елемента (для async операцій)
// components/LoginForm/LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  it('відображає поля email та пароль', () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/пароль/i)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /вхід/i })).toBeInTheDocument();
  });

  it('викликає onSubmit з введеними даними', async () => {
    const user = userEvent.setup();
    const handleSubmit = vi.fn();

    render(<LoginForm onSubmit={handleSubmit} />);

    await user.type(screen.getByLabelText(/email/i), '[email protected]');
    await user.type(screen.getByLabelText(/пароль/i), 'password123');
    await user.click(screen.getByRole('button', { name: /вхід/i }));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'password123',
    });
  });

  it('показує помилку при порожному email', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={vi.fn()} />);

    await user.click(screen.getByRole('button', { name: /вхід/i }));

    expect(screen.getByText(/введіть email/i)).toBeInTheDocument();
  });
});

Моки та провайдери

Компоненти в реальних програмах залежать від контексту — маршрутизатор, сховище, i18n, query-клієнт. Загальний паттерн — кастомний render:

// src/test/render.tsx
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { ReactNode } from 'react';

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });
}

export function renderWithProviders(
  ui: React.ReactElement,
  options?: RenderOptions & { initialEntries?: string[] }
) {
  const { initialEntries = ['/'], ...rest } = options ?? {};
  const queryClient = createTestQueryClient();

  function Wrapper({ children }: { children: ReactNode }) {
    return (
      <QueryClientProvider client={queryClient}>
        <MemoryRouter initialEntries={initialEntries}>
          {children}
        </MemoryRouter>
      </QueryClientProvider>
    );
  }

  return render(ui, { wrapper: Wrapper, ...rest });
}

Тестування асинхронних компонентів

// components/UserProfile/UserProfile.test.tsx
import { renderWithProviders } from '@/test/render';
import { screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';

it('загружує та відображає дані користувача', async () => {
  renderWithProviders(<UserProfile userId="42" />);

  // Спочатку повинен бути індикатор завантаження
  expect(screen.getByRole('progressbar')).toBeInTheDocument();

  // Чекаємо появи даних
  expect(await screen.findByText('Test User')).toBeInTheDocument();
  expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});

Тестування форм з React Hook Form

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

describe('ProductForm', () => {
  it('валідує обов'язкові поля перед відправкою', async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();

    render(<ProductForm onSubmit={onSubmit} />);

    // Порожня відправка
    await user.click(screen.getByRole('button', { name: /зберегти/i }));

    await waitFor(() => {
      expect(screen.getByText(/назва обов'язкова/i)).toBeInTheDocument();
    });

    expect(onSubmit).not.toHaveBeenCalled();
  });

  it('відправляє коректні дані', async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();

    render(<ProductForm onSubmit={onSubmit} />);

    await user.type(screen.getByLabelText(/назва/i), 'Новий продукт');
    await user.type(screen.getByLabelText(/ціна/i), '1500');
    await user.selectOptions(screen.getByLabelText(/категорія/i), 'electronics');

    await user.click(screen.getByRole('button', { name: /зберегти/i }));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        name: 'Новий продукт',
        price: 1500,
        category: 'electronics',
      });
    });
  });
});

Часовий графік

Написати перші тесты для існуючого проекту з нуля: налаштування окружень + базові тесты ключових компонентів — 3–5 днів. Покрити тестами новий компонент середної складності (форма, список, діалог) — 4–8 годин залежно від кількості станів.