Многие программисты при выборе между интеграционным и юнит-тестом отдают предпочтение юнит-тесту (или, иными словами, модульному тесту). Некоторые считают интеграционные тесты антипаттерном, некоторые просто следуют модным тенденциям. Но давайте посмотрим, к чему это приводит. Для реализации юнит-теста mock-объекты навешиваются не только на внешние сервисы и хранилища данных, но и на классы, реализованные непосредственно внутри программы. При этом, если мокируемый класс используется в нескольких других классах, то и mock-объект будет содержаться в тестах на несколько классов. А поскольку тестируемое поведение принято задавать внутри теста (смотри given-when-then, arrange-act-assert, test builder), то поведение моки каждый раз заново задаётся в каждом тесте, и нарушается принцип DRY (хотя дублирования кода может и не быть). Кроме того, поведение класса декларируется в mock-объекте, но сама эта декларация не проверяется, поэтому со временем задекларированное в моке поведение может устареть и начать отличаться от реального поведения мокируемого класса. Это вызывает целый ряд сложностей:
1)Во-первых, при изменении функционала сложно вообще вспомнить, что помимо класса и тестов на него нужно изменить ещё и моки этого класса. Давайте рассмотрим цикл разработки в рамках TDD: «созданиеизменение тестов на функционал -> созданиеизменение функционала -> рефакторинг». Mock-объекты являются декларированием поведения класса и не имеют отношения ни к одной из этих трёх категорий (не являются тестами на функционал, несмотря на то, что в тестах используются, и уж тем более не являются самим функционалом). Таким образом, изменение mock-объектов классов, реализованных внутри программы, не укладывается в концепцию TDD.
2)Во-вторых, сложно найти все места мокирования этого класса. Я не встречал ни одного инструмента для этого. Тут можно или написать свой велосипед, или смотреть все места использования этого класса и отбирать те, где создаются моки. Но при неавтоматизированном поиске можно и ошибиться, проглядеть что-нибудь. Тут у вас, наверное возник вопрос: если проблема столь фундаментальна, как описывает автор, неужели никому не пришло в голову реализовать инструменты, упрощающие её решение? У меня есть гипотеза на этот счёт. Несколько лет назад я начал писать библиотеку, которая должна была собирать mock-объект так же, как IOC-контейнер собирает обычный класс, и автоматически создавать и прогонять тесты на поведение, описываемое в моках. Но затем я отказался от этой идеи, потому что нашёл более элегантное решение проблемы моков: просто не создавать эту проблему. Вероятно, по схожей причине специализированный инструмент для поиска моков конкретного класса или не реализован, или малоизвестен.
3)В-третьих, мест мокирования класса может быть много, и изменение их всех — рутинное занятие. Если программист вынужден делать рутину, которую невозможно автоматизировать, то это явный признак того, что с инструментами, архитектурой или рабочими процессами что-то не в порядке.
Надеюсь, суть проблемы ясна. Далее я опишу пути решения этой проблемы и расскажу, почему, с моей точки зрения, интеграционные тесты предпочтительнее юнит-тестов.