Многие программисты, выбирая между интеграция И модульный тест отдайте предпочтение юнит-тесту (или, другими словами, юнит-тесту).
Некоторые считают интеграционные тесты антипаттерном, а другие просто следуют модным тенденциям.
Но давайте посмотрим, к чему это приведет. Для реализации модульного теста макетные объекты прикрепляются не только к внешним сервисам и хранилищам данных, но и к классам, реализованным непосредственно внутри программы.
В которой, если макетный класс используется в нескольких других классах, то макетный объект также будет содержаться в тестах для нескольких классов .
А поскольку тестируемое поведение обычно указывается внутри теста (см.
дано-когда-то , организовать-действовать-утверждать , конструктор тестов ), то поведение мока каждый раз в каждом тесте перенастраивается, и принцип нарушается СУХОЙ (хотя дублирования кода может и не быть).
Кроме того, в мок-объекте объявляется поведение класса, но само это объявление не проверяется, поэтому со временем поведение, заявленное в мокке, может устареть и начать отличаться от реального поведения мокируемого класса.
Это вызывает ряд трудностей: 1) Во-первых, при изменении функционала сложно помнить, что помимо класса и тестов к нему нужно менять еще и моки этого класса.
Давайте посмотрим на цикл разработки в рамках TDD: «создать\изменить функциональные тесты -> создать\изменить функционал -> рефакторинг».
Мок-объекты являются декларациями поведения классов и не относятся ни к одной из этих трех категорий (они не являются тестами на функциональность, несмотря на то, что они используются в тестах, и уж точно не сама функциональность).
Таким образом, изменение мок-объектов классов, реализованных внутри программы, не укладывается в концепцию TDD .
2)Во-вторых, для этого класса сложно найти все места для насмешек.
Я не нашел для этого никакого инструмента.
Здесь вы можете либо написать свой велосипед, либо посмотреть все места, где используется этот класс, и выбрать те, где создаются моки.
Но при ручном поиске можно ошибиться и что-то упустить.
Тут у вас наверняка возник вопрос: если проблема настолько фундаментальна, как описывает автор, неужели никому не пришло в голову реализовать инструменты, упрощающие ее решение? У меня есть гипотеза на этот счет. Несколько лет назад я начал писать библиотеку, которая собирала бы макет объекта так же, как IOC-контейнер собирает обычный класс, и автоматически создавала бы и запускала тесты на поведение, описанное в макетах.
Но потом я отказался от этой идеи, так как нашел более элегантную решение ложной проблемы: просто не создавайте проблему .
Вероятно, по аналогичной причине специализированный инструмент для поиска моков конкретного класса либо не реализован, либо малоизвестен.
3) В-третьих, мест для макания класса может быть много, и менять их все — рутинная задача.
Если программист вынужден выполнять рутину, которую невозможно автоматизировать, то это явный признак того, что что-то не так с инструментами, архитектурой или рабочими процессами.
Надеюсь, суть проблемы ясна.
Далее я опишу способы решения этой проблемы и расскажу, почему, с моей точки зрения, интеграционные тесты предпочтительнее модульных.
В качестве решения проблемы предлагаю использовать мокинг только для внешних сервисов и хранилищ данных, а в остальных случаях использовать реальные классы, т.е.
писать интеграционные тесты вместо модульных тестов.
Некоторые программисты скептически относятся к интеграционным тестам, и им эта идея не нравится.
Давайте посмотрим, какие аргументы приводят противники интеграционных тестов.
Утверждение 1. Интеграционные тесты менее полезны при поиске ошибок, чем модульные тесты.
Доказательство: Представим, что в каком-то классе, который используется повсеместно, допущена ошибка.
После этого красным цветом стали тесты самого класса, а также все интеграционные тесты, в которых этот класс использовался.
В результате половина тестов в проекте красные.
Как понять причину покраснения анализов? С какого теста мне начать? А вот если бы вместо класса использовался его макетный объект, то красными стали бы только тесты этого класса.
Опровержение: Давайте вспомним рабочий процесс внутри TDD: «красные» тесты, сигнализирующие об ошибке -> создание/изменение функционала -> «зеленые» тесты.
Соответственно, при изменении функционала программист сначала меняет тесты, чтобы они проверяли измененный функционал.
Поскольку код все еще содержит устаревший функционал, тесты не проходят. Затем программист редактирует функциональный код, и тесты проходят. Если программист работал с классами, а не с их тестами, то он не действовал в рамках TDD. Но даже если программист изменил код, но не изменил тесты и не проверил их прохождение, то провал тестов можно отследить с помощью сервера непрерывной интеграции, который автоматически запускает тесты при каждом обращении к системе контроля версий.
.
Автор изменений увидит сообщение о провале теста, сразу запомнит, какие классы он редактировал, и в первую очередь начнет разбираться с тестами именно этих классов.
Если программист непреднамеренно внес ошибку в определенный класс, а затем исправил ее, то зеленым станут не только тесты этого класса, но и все тесты, в которых этот класс использовался.
Но что, если они не станут зелеными? Тогда это сигнал о том, что изменения в классе привели к изменению поведения других классов, где этот класс использовался, и теперь либо в этих классах появились ошибки, либо их тесты отклонились от логики приложения.
Возможен и другой случай.
Если по каким-то причинам класс, в котором произошла ошибка, не был хорошо покрыт тестами, то юнит-тесты на моках вообще не выявили бы проблему.
Интеграционные тесты как минимум сигнализируют о проблеме, хотя для выявления проблемного класса придется прибегнуть к старой доброй трассировке.
Подведем итог: если вы следуете TDD, то выделение красным цветом тестов классов, которые вы не меняли, является преимуществом, поскольку сигнализирует о проблемах.
Если вы не следуете TDD, а используете непрерывную интеграцию, то избыточные тесты для вас не такая уж проблема.
Если вы не следите за TDD и не проводите тесты регулярно, то для вас актуальна проблема выявления соответствия между «проваленным тестом и проблемным классом».
В этом случае проблему дублирования знаний в моках и отсутствия тестов на заявленное в моках поведение лучше решать не интеграционными тестами вместо юнит-тестов, а другими средствами (мы поговорим о их чуть позже).
Утверждение 2. Интеграционные тесты менее полезны при проектировании, чем модульные тесты.
Доказательство: Модульное тестирование, в отличие от интеграционного тестирования, заставляет программистов внедрять зависимости через конструктор или свойства.
А если вы используете интеграционное тестирование вместо модульного тестирования, то джуниор может создавать экземпляры зависимостей прямо в коде класса.
И у меня очень мало времени для написания архитектурных заметок и проведения проверок кода.
Да и доверить некому.
И я не хочу.
Опровержение: На самом деле не только модульное тестирование может заставить программиста внедрять зависимости.
IOC-контейнер делает это очень хорошо.
Фактически, если вы внедряете зависимости, вы, вероятно, используете IOC-контейнер.
Можно, конечно, самому написать фабрику для создания самого важного класса, содержащего точку входа.
Но IOC-контейнер решает многие распространенные проблемы и облегчает жизнь.
Например, вы можете сделать класс одноэлементным с помощью одной строчки кода, не углубляясь в подводные скалы Реализации синглтона.
Итак, если вы внедряете зависимости, но не используете IOC-контейнер, я рекомендую начать это делать.
В общем, если вы используете модульное тестирование, вы почти наверняка используете IOC-контейнер.
Если вы используете IOC-контейнер, он побуждает программиста внедрять зависимости.
Конечно, вы можете создать объект без использования IOC-контейнера, но вы также можете создать класс, не проводя для него модульного теста.
Итак, я не вижу какого-либо существенного преимущества в модульных тестах с точки зрения поощрения соблюдения принципа инверсии управления.
Кроме того, вам не придется заставлять программистов делать то, что вы хотите из-за архитектурных ограничений, а просто объяснять преимущества внедрения зависимостей и использования IOC-контейнера.
Принуждение силой , как и любое насилие, может вызвать контрсопротивление.
Утверждение 3. Чтобы покрыть один и тот же функционал тестами, вам понадобится гораздо больше интеграционных тестов, чем модульных тестов.
Доказательство: Автор статьи с громким заголовком «Интеграционные тесты — для жуликов» пишет, что ненавидит интеграционные тесты со всей страстью и считает их вирусом, приносящим бесконечную боль и страдания.
Свои мысли он обосновывает следующим образом:
Вы пишете интеграционные тесты, потому что не можете писать идеальные модульные тесты.Опровержение: Автор этой статьи дает следующее определение интеграционного теста:Вы знаете эту проблему: все ваши тесты проходят успешно, но программа по-прежнему показывает дефект. Вы решаете написать интеграционный тест, чтобы убедиться, что весь путь выполнения программы работает должным образом.
И вроде бы все идет хорошо, пока не думаешь: «А давайте везде использовать интеграционные тесты».
Плохая идея! Количество возможных путей выполнения программы нелинейно зависит от размера программы.
Чтобы покрыть тестами веб-приложение в 20 страниц, вам понадобится не менее 10 000 тестов.
Возможно, миллион.
Если вы будете писать 50 тестов в неделю, то в год вы напишете только 2500 тестов, что составляет 2,5% от необходимого количества.
И после этого вы задаетесь вопросом, почему вы тратите 70% своего времени, отвечая на звонки пользователей?! Интеграционные тесты — пустая трата времени.
Они должны остаться в прошлом.
Я использую термин «интегрированный тест» для обозначения любого теста, результат которого (пройден или не пройден) зависит от правильности реализации более чем одной части нетривиального поведения.Как видите, в этом определении нет ни слова о том, что интеграционные тесты можно писать только на основном классе, в котором находится точка входа, но автор вышеупомянутой статьи в своих рассуждениях неявно опирается на это условие.Интеграционный тест — это тест, результат прохождения которого зависит от корректной реализации более чем одного фрагмента нетривиальной логики (метода).
Согласно TDD, тесты предназначены для проверки функциональности, а не путей выполнения программы.
Следуйте TDD, и вы не столкнетесь с проблемами, о которых говорил этот автор.
Просто пишите интеграционные тесты так же, как вы пишете модульные тесты, но не высмеивайте классы, реализованные в вашей программе, и вы не столкнетесь с проблемой экспоненциального увеличения количества тестов.
Утверждение 4. Интеграционные тесты занимают больше времени, чем модульные.
К сожалению, с этим не поспоришь — интеграционные тесты почти всегда занимают больше времени, чем модульные.
Создание макета, конечно, не бесплатное и требует некоторого времени, но логика приложения обычно занимает больше времени.
Гипотетически вполне возможно, что тесты выполняются неудовлетворительно по времени, и оптимизировать тестируемую логику в ближайшее время вы не собираетесь.
И вполне логичным решением могла бы стать оптимизация тестов.
Например, используя моки.
Способы борьбы с дублированием и устареванием знаний в моках
Первый способ, как я уже говорил, — использовать моки только для объявления поведения внешних сервисов и хранилищ данных.Второй способ — автоматическая проверка релевантности поведения, заявленного в моке.
Например, вы можете автоматически создать и запустить соответствующий тест. Но тогда нужно учитывать, что у мокируемого класса могут быть свои зависимости, некоторые из которых могут быть внешними сервисами.
По соображениям производительности вы можете сначала протестировать уникальное поведение (заданное в макетах) классов самого нижнего уровня, затем поведение классов, использующих предыдущие классы, и так далее.
Тогда, если в моках в нескольких местах заявлено какое-то одинаковое поведение, то протестировать его можно только один раз.
Для каждого уникального случая мокинга можно вручную написать тест и каким-то образом задать соответствие между мокингом и тестом для него, а также поручить программистам вручную поддерживать это соответствие при изменении функционала.
Вы можете просто поручить программистам вручную обновлять макеты объектов.
Но тогда вам придется немного изменить рабочий процесс, отойдя от классического TDD, заменив «Изменение тестов на функциональность -> Изменение функциональности -> .
» на «Изменение тестов на функциональность -> Изменение деклараций этого поведения (в моках ) -> Изменение функционала -> .
" Чтобы исключить проблему дублирования кода при моке, можно разместить все моки для одного класса в отдельном хранилище.
Это упростит этап «Изменение деклараций поведения в моках», но может снизить читабельность модульного теста — решайте сами, исходя из собственных приоритетов.
Заключение
Мартин Фаулер много лет назад заметил образование двух разных школ TDD - классическая школа и мокисты:Теперь я нахожусь на том этапе, когда могу исследовать вторую дихотомию: между классическим и мокистским TDD. Большая проблема здесь заключается в том, когда использовать макет (или другой дубль).Обе эти школы имеют свои преимущества и недостатки.Классический стиль TDD заключается в использовании реальных объектов, если это возможно, и двойных объектов, если использовать реальные объекты неудобно.
Таким образом, классический TDDer будет использовать реальный склад и дубль для почтовой службы.
Вид двойника не имеет особого значения.
Однако сторонник TDD-макета всегда будет использовать макет для любого объекта с интересным поведением.
При этом и для склада, и для почтовой службы.
Лично я считаю, что недостатки классического TDD более приемлемы и решаемы, чем недостатки мокрого TDD. Ну а кто-то может подумать наоборот — они прекрасно могут справиться с последствиями использования мокрого TDD и не считать приемлемыми проблемы, возникающие при классическом TDD. Почему нет? Все люди разные, и каждый имеет право на свой стиль.
Я лишь привел причины, почему лично мне больше нравится классика, но окончательный выбор за вами.
P.S. Я не призываю вас полностью отказаться от модульных тестов.
При использовании классического TDD тесты для тех классов, которые не обращаются к методам и свойствам других классов, будут модульными.
Теги: #tdd #интеграционное тестирование #mock #tdd
-
Вустер, Джозеф Эмерсон
19 Oct, 24 -
Книги Для «Прокачки» Мозга
19 Oct, 24 -
Дайджест Kolibrios №3: Начало Весны
19 Oct, 24 -
И Еще Один Релиз: Qt 5.0 Rc.
19 Oct, 24