Сколько раз ты открывал компонент в рабочем проекте на React и видел гремучую смесь из десятка useState, условий загрузки, проверки прав доступа и километров JSX-разметки? В React 19 компоненты по-прежнему часто превращаются в неповоротливых монстров, которых мучительно тяжело поддерживать и практически невозможно тестировать без боли.
Когда ты пишешь тесты на такой компонент через стандартный render из React Testing Library, тест глубоко проваливается во все дочерние узлы. Любая правка стилей или внутренняя перестановка в маленькой карточке ломает тесты родительской панели на десятки экранов выше.
Инженер Евгений Плотников (@bad4iz) предложил элегантное решение этой проблемы — архитектурный шаблон useSetup[Component], вдохновленный секцией setup во Vue 3, в связке с легковесной библиотекой поверхностного тестирования Shallowly для Vitest.
Разделение обязанностей: хук для логики, компонент для JSX
Идея проста: компонент должен оставаться чистой визуальной функцией. Вся логика подготовки данных, состояния загрузки и преобразования моделей выносится в персональный хук с префиксом useSetup:
// src/useSetupProfilePanel.ts
import { useProfile } from './useProfile';
export function useSetupProfilePanel() {
const state = useProfile();
if (state.status === 'loading') {
return {
isLoading: true as const,
userCardProps: null,
};
}
return {
isLoading: false as const,
userCardProps: {
name: state.profile.name,
},
};
}
А теперь посмотри, насколько прозрачным и читаемым становится сам компонент:
// src/ProfilePanel.tsx
import { UserCard } from './UserCard';
import { useSetupProfilePanel } from './useSetupProfilePanel';
export function ProfilePanel() {
const { isLoading, userCardProps } = useSetupProfilePanel();
if (isLoading) {
return <p role="status">Loading profile...</p>;
}
return (
<section aria-label="Profile">
<h1>Your profile</h1>
<UserCard {...userCardProps} />
</section>
);
}
Компонент больше ничего не знает об источниках данных. Он просто берет готовые props и раскладывает их по дереву элементов.
Трехуровневая стратегия тестирования
При такой структуре тестирование раскладывается на три четких уровня без дублирования проверок:
- Тест логики хука: тестируем
useSetupProfilePanelчерезrenderHookс подменой сетевых ответов. - Поверхностный (shallow) тест разметки: проверяем, что родитель корректно передает пропсы в дочерний узел
UserCardи рендерит атрибуты доступности (role,aria-label). - DOM-тест интеграции: запускаем только там, где нужно проверить сквозной сценарий пользователя.
Для второго уровня на помощь приходит библиотека Shallowly (современная замена старому Enzyme, адаптированная под React 18/19 и Vitest):
// src/ProfilePanel.shallow.test.tsx
import { shallow } from 'shallowly';
import { describe, expect, it, vi } from 'vitest';
import { ProfilePanel } from './ProfilePanel';
import { UserCard } from './UserCard';
import { useSetupProfilePanel } from './useSetupProfilePanel';
vi.mock('./useSetupProfilePanel');
describe('ProfilePanel shallow tests', () => {
it('передает подготовленные свойства в дочернюю карточку', () => {
const userCardProps = { name: 'Алексей' };
useSetupProfilePanel.mockReturnValue({ isLoading: false, userCardProps });
const wrapper = shallow(<ProfilePanel />);
expect(wrapper.find(UserCard).props()).toEqual(userCardProps);
});
it('отображает статус загрузки без рендеринга карточки', () => {
useSetupProfilePanel.mockReturnValue({ isLoading: true, userCardProps: null });
const wrapper = shallow(<ProfilePanel />);
expect(wrapper.find('p').props()).toEqual({
role: 'status',
children: 'Loading profile...',
});
expect(wrapper.find(UserCard).exists()).toBe(false);
});
});
В тесте Shallowly компонент UserCard даже не инициализируется: его хуки не запускаются, сетевые запросы не отправляются, а тест выполняется за считанные миллисекунды.
Как запустить демо-проект локально
Для проверки подхода на Node.js 22 или 24 достаточно склонировать официальный репозиторий с тегом статьи и запустить тесты:
git clone --branch profile-panel-article-v3 --single-branch https://github.com/bad4iz/unit-test-article.git
cd unit-test-article/examples/profile-panel
npm ci && npm test && npm run typecheck
Все 6 тестов (включая shallow, хуки и DOM) проходят за доли секунды без единой ошибки типов.
Такой подход требует дисциплины при создании файлов, но окупается мгновенно: тесты перестают быть хрупкими, а логика и разметка живут независимо друг от друга.
