Представьте себе сборочный цех (тестируемый класс). Рабочий (метод) должен собрать устройство, используя детали от поставщиков (зависимости). Моки — это не реальные поставщики, а актеры, которые лишь изображают их, следуя жесткому сценарию. **Первый недостаток: тесты становятся хрупкими и привязанными к реализации.** Вообразите видео, где вы меняете способ взаимодействия с поставщиком: вместо одного вызова `supplier.getDetail(partId)` теперь делается два: `supplier.checkAvailability(partId)` и затем `supplier.orderDetail(partId)`. Ваш старый тест с моком, который верифицировал один вызов `getDetail`, немедленно падает, хотя публичный контракт класса (собрать устройство) и его конечный результат не изменились. Тест ломается из-за изменения внутренней реализации, а не из-за сломанной функциональности. Это приводит к высоким затратам на поддержку тестов и страху вносить изменения.
**Второй недостаток: ложное чувство безопасности.** Мок точно знает, что ему нужно вернуть по сценарию. Но представьте, что реальный поставщик (зависимость) со временем изменил формат возвращаемой детали или начал выбрасывать исключение в новых условиях. Ваш мок, жестко запрограммированный на старый формат, этого не имитирует. Видео: на производстве приходит новая партия деталей с измененным креплением, но актер-поставщик продолжает вручать старые. Сборка в тесте проходит успешно, а в продакшене — катастрофа. Тест с моком не проверяет интеграцию с реальным контрактом зависимости (интерфейсом или API), создавая опасную иллюзию работоспособности.
**Третий недостаток: чрезмерная специфичность и сложность.** Когда класс имеет много зависимостей (5-10), тест превращается в кошмар настройки. Мысленное видео: перед началом съемки (теста) режиссер (разработчик) полчаса инструктирует каждого актера-мока (mock(dependency1), mock(dependency2)...), что именно говорить, когда и сколько раз. Сам сценарий теста тонет в шуме настройки (`when(...).thenReturn(...)`), и бизнес-логика, которую нужно проверить, становится едва различимой. Такой тест сложно читать, понимать и изменять.
**Четвертый недостаток: моки не подходят для тестирования взаимодействия с фреймворками или сложными библиотеками.** Попытка замокать `JdbcTemplate`, `RestTemplate`, `Hibernate Session` или `KafkaTemplate` — это путь в ад. Поведение этих компонентов сложное, с нюансами (транзакции, исключения, состояние соединения). Мок, имитирующий лишь 10% этого поведения, даст нерелевантный результат. Эксперты в таких случаях рекомендуют использовать **реальные, но легковесные замены**: in-memory базу данных (H2), embedded Kafka, WireMock для HTTP или же полноценные интеграционные тесты с Testcontainers.
**Что же предлагают эксперты?** Альтернативы и правила применения.
- **Отдавайте предпочтение реальным объектам, где это возможно.** Используйте **стабы (stubs)** или **фейки (fakes)** — упрощенные, но работающие реализации. Например, in-memory репозиторий вместо мока `UserRepository`. Это проверяет реальную логику взаимодействия.
- **Применяйте моки только для «нестабильных» зависимостей** — внешних сервисов, шлюзов, сложных сторонних клиентов, которые действительно нужно изолировать.
- **Тестируйте поведение, а не взаимодействие.** Вместо `verify(mock, times(1)).someMethod()` чаще проверяйте конечное состояние системы или выходные данные. Это делает тесты устойчивее к рефакторингу.
- **Используйте «классический» подход к юнит-тестированию** (из книги «xUnit Test Patterns»), где тестируется один класс вместе с его реальными зависимостями (где это дешево), а мокаются только «удобрения» (doc dependencies). Современная школа «Лондонского стиля» (мокать все соседей) признана многими экспертами излишне жесткой.
- **Смещайте фокус на интеграционные и сквозные тесты.** Критическую бизнес-логику, особенно involving несколько компонентов, лучше проверять в тестах, которые используют реальные или приближенные к реальным зависимости. Моки же оставить для узких, изолированных юнит-тестов сложных алгоритмов.
Комментарии (12)