Тестирование деталей реализации.
Тест проверяет то, как код работает изнутри — приватные поля, внутренние вызовы методов, структуру DOM или CSS-классы — вместо наблюдаемого поведения, от которого зависит реальный потребитель.
##Signs and Symptoms
Вы распознаёте этот запах, когда тест дотягивается за публичный контракт и фиксирует механику за ним. Частые признаки:
- Проверки приватного/внутреннего состояния или полей либо прямой вызов приватных методов (часто через приведения типов, рефлексию или
// @ts-expect-error). - Слежка за тем, что внутренний хелпер был вызван, или проверка этого (
expect(internalCalc).toHaveBeenCalled()) вместо проверки результата. - UI-тесты, ищущие по CSS-классу, тегу, хукам
data-*или позиции в DOM, а не по роли/метке/тексту — например,container.querySelector('.btn-primary > span:nth-child(2)'),wrapper.state(),wrapper.find('SomeChildComponent').props(). - Снапшот-тесты по целым деревьям рендера / сериализованным внутренним объектам, так что любая правка разметки приводит к падению.
- Тесты, которые ломаются при каждом рефакторинге, хотя функция всё ещё работает (ложные отрицания), и наоборот продолжают проходить после логической ошибки, потому что лишь перепроверяют обвязку (ложные срабатывания).
// ЗАПАХ: связывает тест с внутренностями компонента и структурой DOM
test('counter increments', () => {
const wrapper = mount(<Counter />);
wrapper.instance().handleClick(); // напрямую вызывает приватный метод
expect(wrapper.state('count')).toBe(1); // проверяет внутреннее состояние
expect(wrapper.find('.count-display').text()).toBe('1'); // хрупкий CSS-селектор
});
Та же форма проявляется и на стороне сервера: expect(service._cache.size).toBe(1) или проверка точной последовательности внутренних вызовов, которые делает метод.
##Reasons for the Problem
Почему это происходит
- До внутреннего состояния легко дотянуться — публичное поле, экспортируемый хелпер или
container.querySelectorпрямо под рукой, тогда как прогон реального поведения требует больше подготовки. - Погоня за метриками покрытия: тестирование каждого приватного метода один-к-одному кажется тщательным.
- Обильное использование моков толкает людей к проверке «был ли вызван этот коллаборатор» вместо «произошло ли нужное».
- Инструменты, которые это поощряют: поверхностный рендеринг (shallow rendering) / API
instance()/state()или захват узлов по имени класса.
Почему это вредно
- Сопровождаемость / хрупкость. Это хрупкий тест (Fragile Test) Месароша, вызванный переспецифицированным ПО (Overspecified Software): тест фиксирует поведение, которого потребитель никогда не требовал, поэтому безобидные рефакторинги (переименование метода, реструктуризация разметки, изменение приватного поля) ломают зелёные тесты без реальной причины. Тесты становятся налогом на рефакторинг, а не страховочной сеткой.
- Ложная уверенность (главная опасность, по Кенту Ч. Доддсу). Тесты деталей реализации ошибаются в обе неверные стороны: ложные отрицания (тест краснеет, хотя функция всё ещё работает) и ложные срабатывания (тест остаётся зелёным, хотя функция сломана — вы проверили обвязку, а не результат). В любом случае набор перестаёт говорить вам правду.
- Читаемость. Тест документирует, как код устроен, а не что он гарантирует. Читатель не может понять, какое поведение действительно важно, и тест больше не служит примером использования или спецификацией.
- Связанность. Он замораживает текущие решения о дизайне, отбивая охоту именно к тем рефакторингам, которые тесты должны делать безопасными.
##Treatment
Тестируйте через публичный контракт — ту же поверхность, которой касается реальный вызывающий или пользователь, — и проверяйте наблюдаемый вывод: возвращаемые значения, выброшенные ошибки, испущенные события, сохранённое состояние или отрисованный/видимый UI.
- Определите потребителя. Для модуля это его экспортируемый API; для UI-компонента это пользователь (клики, ввод) и то, что он может видеть.
- Подавайте входные данные так, как это делал бы потребитель, а не вызовом приватных методов. Инициируйте реальный клик вместо вызова обработчика; вызывайте публичный метод вместо хелпера.
- Проверяйте результаты, а не внутренности. Замените проверки
state()/приватного поля/toHaveBeenCalledпроверками того, что выходит наружу. - Ищите элементы UI по доступности, а не по структуре — по роли, метке, тексту — а не по CSS-классам, тегам или
nth-child. - Перестаньте тестировать приватные методы напрямую. Покрывайте их через публичный метод, который их использует; если приватная единица достаточно сложна, чтобы нуждаться в собственных тестах, это сигнал вынести её в отдельный модуль с собственным публичным API.
- Приберегите проверки моков/шпионов для настоящих границ (сеть, время, платёжный шлюз), где сам вызов и есть наблюдаемое поведение, — а не для внутренних коллабораторов.
// ДО: тестирует детали реализации
const wrapper = mount(<Counter />);
wrapper.instance().handleClick();
expect(wrapper.state('count')).toBe(1);
expect(wrapper.find('.count-display').text()).toBe('1');
// ПОСЛЕ: тестирует наблюдаемое поведение через публичный, обращённый к пользователю контракт
render(<Counter />);
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('1')).toBeInTheDocument();
Эмпирическое правило: если рефакторинг, сохраняющий поведение, ломает тест, значит тест проверял деталь реализации.
##Detected by
- eslint-testing-library testing-library/no-node-access — no-node-access
- eslint-testing-library testing-library/no-container — no-container