Тесты, которые не выдерживают изменений в приложении, называются хрупкими. Они легко ломаются при изменении структуры, стилей или мелких деталей реализации, что делает их неэффективными и трудозатратными. Важно помнить, что тесты должны помогать улучшать качество кода, а не создавать дополнительные проблемы. В React Testing Library основной целью является создание тестов, которые проверяют функциональность компонента с точки зрения пользователя, а не его внутреннюю реализацию. Это помогает избежать хрупкости тестов и делает их более устойчивыми к изменениям.
Основной принцип React Testing Library — тестировать поведение компонента, а не его внутреннюю структуру или детали реализации. Это помогает создавать тесты, которые не ломаются при изменении стилей, классов или структуры HTML-разметки.
Пример:
// Плохой тест: зависит от структуры DOM
test('должен отображать текст в <div> с классом "message"', () => {
render(<Message text="Привет, мир!" />);
const messageElement = screen.getByText(/Привет, мир!/);
expect(messageElement).toHaveClass('message');
});
Этот тест может сломаться, если структура изменится, даже если компонент продолжает работать корректно.
Хороший тест:
// Хороший тест: проверка текста
test('должен отображать переданный текст', () => {
render(<Message text="Привет, мир!" />);
const messageElement = screen.getByText(/Привет, мир!/);
expect(messageElement).toBeInTheDocument();
});
Этот тест проверяет только поведение компонента (отображение текста), а не его структуру.
Использование классов и стилей для идентификации элементов может быть источником хрупкости тестов. Если стили или классы изменяются, тесты могут начать не проходить, даже если функциональность не нарушена. Лучше использовать текст, атрибуты и роли, которые менее подвержены изменениям.
Пример:
// Плохой тест: зависит от класса для выбора элемента
test('кнопка должна быть синей', () => {
render(<Button label="Отправить" />);
const button = screen.getByRole('button', { name: /отправить/i });
expect(button).toHaveClass('btn-blue');
});
Этот тест сломается, если класс кнопки изменится.
Лучше использовать поведение:
// Хороший тест: проверка доступности кнопки
test('кнопка должна быть активной и доступной для клика', () => {
render(<Button label="Отправить" />);
const button = screen.getByRole('button', { name: /отправить/i });
expect(button).not.toBeDisabled();
});
Здесь тест проверяет функциональность кнопки, а не её стиль.
Одним из подходов к улучшению тестов является использование
семантических элементов HTML и атрибутов доступности. Это помогает
создать более стабильные и читаемые тесты. Например, использование
aria-label, role или alt для
элементов, которые важно идентифицировать для тестирования, повышает их
устойчивость.
Пример:
// Плохой тест: использование CSS-селекторов для поиска
test('пользователь может закрыть окно модального диалога', () => {
render(<Modal />);
const closeButton = screen.getByText('Закрыть');
fireEvent.click(closeButton);
expect(screen.queryByText('Закрыто')).toBeInTheDocument();
});
Зависимость от текста может быть хрупкой, если текст изменится или будет переведен.
Лучше использовать доступность:
// Хороший тест: использование aria-label
test('пользователь может закрыть окно модального диалога', () => {
render(<Modal />);
const closeButton = screen.getByRole('button', { name: /закрыть/i });
fireEvent.click(closeButton);
expect(screen.queryByText('Закрыто')).toBeInTheDocument();
});
Теперь тест проверяет кнопку с помощью атрибута доступности, который более устойчив к изменениям.
React Testing Library предлагает использовать библиотеку
user-event для симуляции пользовательских действий, вместо
прямого использования fireEvent. user-event
более точно имитирует поведение пользователя и помогает избежать хрупких
тестов, связанных с синтаксисом событий.
Пример с fireEvent:
// Плохой тест: использование fireEvent
test('проверка клика на кнопку', () => {
render(<Button />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(screen.getByText('Кнопка нажата')).toBeInTheDocument();
});
Хотя этот тест работает, fireEvent не всегда точно
имитирует реальные действия пользователя.
Использование user-event:
// Хороший тест: использование user-event
import userEvent from '@testing-library/user-event';
test('проверка клика на кнопку', () => {
render(<Button />);
const button = screen.getByRole('button');
userEvent.click(button);
expect(screen.getByText('Кнопка нажата')).toBeInTheDocument();
});
Этот подход более стабилен и имитирует действия пользователя с большей точностью.
Когда тестируется компонент, который зависит от внешних сервисов или API, важно избегать жесткого мокирования конкретных деталей реализации. Вместо того чтобы напрямую изменять состояние компонента или сервисов, лучше абстрагироваться через контексты или хук для мокирования поведения.
Пример с жёстким мокированием:
// Плохой тест: жесткое мокирование
test('должен загружать данные с API', () => {
render(<UserList />);
fetch.mockResolvedValueOnce({ json: () => Promise.resolve({ users: [] }) });
const users = await screen.findByText('Пользователи');
expect(users).toBeInTheDocument();
});
Мокирование на уровне реализации может ломаться при изменениях в API.
Лучше использовать контексты или абстракции для работы с зависимостями:
// Хороший тест: использование контекста или хука
test('должен загружать данные с API', () => {
render(
<DataProvider mockData={{ users: [] }}>
<UserList />
</DataProvider>
);
const users = await screen.findByText('Пользователи');
expect(users).toBeInTheDocument();
});
Такой подход гарантирует, что тест не зависит от конкретной реализации API.
Тесты, которые зависят от внутреннего состояния компонента, могут быть хрупкими, так как любое изменение состояния может привести к поломке тестов. Вместо этого лучше фокусироваться на проверке поведения компонента.
Пример с проверкой состояния:
// Плохой тест: проверка состояния
test('кнопка должна включать и выключать режим темной темы', () => {
render(<ThemeSwitcher />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(button).toHaveTextContent('Темная тема включена');
});
Этот тест зависит от того, как компонент изменяет своё состояние.
Лучше проверять результат действия:
// Хороший тест: проверка поведения
test('кнопка должна менять тему', () => {
render(<ThemeSwitcher />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(document.body).toHaveClass('dark-theme');
});
Здесь тест проверяет, как меняется поведение, а не состояние компонента.
Чтобы избежать хрупких тестов в React, важно следовать принципу
тестирования поведения, а не реализации. Применение семантических
элементов, использование атрибутов доступности, правильная имитация
пользовательских действий с помощью user-event и абстракция
зависимостей через контексты и хуки позволяют создавать тесты, которые
устойчивы к изменениям и остаются полезными на протяжении долгого
времени.