В прошлой статье я показал короткий тест с передачей пропсов. Теперь разберу компонент чуть подробнее: сначала приведу код к тому виду, в котором мне удобно с ним работать, а потом проверю его через свою библиотеку Shallowly.
Начнём с панели профиля. Пока данные грузятся, она показывает сообщение. Когда профиль готов — передаёт имя в карточку. Входных пропсов у самой панели пока нет, усложнять пример ими не будем.
Исходный компонент
import { UserCard } from './UserCard'; import { useProfile } from './useProfile'; export function ProfilePanel() { const state = useProfile(); if (state.status === 'loading') { return <p role="status">Loading profile...</p>; } return ( <section aria-label="Profile"> <h1>Your profile</h1> <UserCard name={state.profile.name} /> </section> ); }
useProfile возвращает либо { status: 'loading' }, либо { status: 'ready', profile }. В примере внутри него обычный useState с локальным профилем. Запросов к бэку здесь нет. UserCard принимает name и умеет раскрывать детали через свой useState.
Тесты и архитектура
Тесты не должны диктовать архитектуру проекта. Если ради удобства одной библиотеки приходится выворачивать рабочий код наизнанку, я бы сначала посмотрел на сам способ тестирования. Но это не мешает менять архитектуру в лучшую сторону. У более понятного кода и тесты часто получаются проще.
В нашей панели вместе живут разметка и подготовка данных для неё. Сейчас это несколько строк. В рабочем компоненте к ним легко добавляются вычисления, проверки прав, обработка ошибок. Всё это приходится читать вперемешку с JSX. Вынесем подготовку данных в хук, чтобы в компоненте было видно, какие элементы он собирает и что им передаёт.
Separation of Concerns — разделение задач. Подготовка данных живёт в хуке, разметка — в компоненте. В JSX больше не нужно разбираться в устройстве результата
useProfile.Single Responsibility Principle — более узкая ответственность. Изменение разметки относится к компоненту, изменение способа подготовки данных — к setup-хуку. Это разные причины менять код.
Инкапсуляция в custom hook. Хук возвращает то, что нужно конкретной панели: признак загрузки и готовые пропсы карточки. Детали источника данных остаются внутри. Такой вынос логики в custom hooks описан и в документации React.
Идею подсмотрел во Vue 3 с его setup. В React предлагаю использовать соглашение useSetup[Component]: название сразу обозначает роль хука — собрать логику и подготовить данные для конкретного компонента. Для ProfilePanel это будет useSetupProfilePanel. В самом компоненте оставляем разметку.
Выносим подготовку данных
Добавим 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, }, }; }
Теперь исходный state.status превращается в понятный панели isLoading, а имя собирается в userCardProps. В состоянии загрузки пропсов карточки ещё нет. as const сохраняет для TypeScript значения true и false в качестве признаков двух вариантов результата. После проверки isLoading он понимает, что userCardProps уже не null.
В 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> ); }
Теперь тестируем компонент
Входных пропсов у ProfilePanel нет: тест рендерит его как <ProfilePanel />. Данные приходят из useSetupProfilePanel, а панель передаёт их дальше — в UserCard через userCardProps. Вот эту передачу и проверим: имя из хука должно попасть в карточку. Заодно проверим заголовок, подпись секции и сообщение о загрузке.
В shallow-тесте замокаю useSetupProfilePanel и задам готовый результат. Как хук получил имя и почему решил, что загрузка закончилась, здесь меня не интересует: это проверим отдельно в тестах хука.
Компонент и хук написаны на TypeScript, тесты ниже — на JavaScript. ProfilePanel.shallow.test.jsx целиком:
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');
/**
* Задаёт готовый профиль для тестов, которым не нужен особый ответ хука.
*
* @returns {void}
*/
const mockDefault = () => {
useSetupProfilePanel.mockReturnValue({
isLoading: false,
userCardProps: { name: 'имя по умолчанию' },
});
};
describe(' 