С точки зрения TDD, я плохой человек, если я проверяю против живой конечной точки, а не надругаться?

16

Я следую TDD религиозно. Мои проекты обычно имеют 85% или лучше тестовое покрытие, со значимыми тестовыми примерами.

Я много работаю с HBase , и основной интерфейс клиента, HTable, очень неприятен. Для написания модульных тестов у меня уходит в 3 или 4 раза больше времени, чем для написания тестов, использующих действующую конечную точку.

Я знаю, что с философской точки зрения тесты, использующие макеты, должны иметь приоритет над тестами, которые используют действующую конечную точку. Но издевательство над HTable - серьезная боль, и я не уверен, что он дает много преимуществ по сравнению с тестированием на живом экземпляре HBase.

Каждый в моей команде запускает экземпляр HBase с одним узлом на своей рабочей станции, и у нас есть экземпляры HBase с одним узлом, работающие на наших коробках Jenkins, так что это не проблема доступности. Тесты живых конечных точек, очевидно, занимают больше времени, чем тесты, использующие макеты, но нас это не волнует.

Прямо сейчас я пишу живые тесты конечных точек И тесты на основе макетов для всех моих классов. Я хотел бы отказаться от насмешек, но я не хочу, чтобы в результате качество ухудшалось.

Что вы все думаете?

хладнокровие
источник
8
Конечная точка на самом деле не является модульным тестом, не так ли? Это интеграционный тест. Но в конечном счете это, вероятно, вопрос прагматизма; вы можете либо тратить время на написание макетов, либо на написание функций или исправление ошибок.
Роберт Харви
4
Я слышал истории о людях, которые отключали сторонние сервисы, выполняя модульное тестирование своего собственного кода ... который был подключен к живой конечной точке. Ограничение скорости - это не то, чем обычно занимается или занимается модульное тестирование.
14
Ты не плохой человек. Ты хороший человек, делаешь плохие вещи.
Kyralessa
15
Я неукоснительно следую TDD Может быть, в этом проблема? Я не думаю, что любая из этой методологии предназначена для того, чтобы воспринимать это всерьез. ;)
FrustratedWithFormsDesigner
9
Следование TDD неукоснительно означало бы, что вы отказываетесь от 15% открытого кода.
Mouviciel

Ответы:

23
  • Моя первая рекомендация - не издеваться над типами, которые вам не принадлежат . Вы упомянули, что HTable - это настоящая боль для насмешек - возможно, вам следует вместо этого обернуть его в адаптер, который предоставляет 20% необходимых вам функций HTable, и при необходимости высмеивать оболочку.

  • При этом давайте предположим, что мы говорим о типах, которыми вы все владеете. Если ваши фиктивные тесты сосредоточены на сценариях счастливого пути, где все идет гладко, вы ничего не потеряете, потому что ваши интеграционные тесты, вероятно, уже тестируют те же самые пути.

    Однако отдельные тесты становятся интересными, когда вы начинаете думать о том, как тестируемая система должна реагировать на каждую мелочь, которая может произойти, как это определено в контракте ее сотрудника, независимо от того, с каким конкретным объектом она разговаривает. Это часть того, что некоторые называют основной правильностью . Таких маленьких кейсов может быть много, а комбинаций гораздо больше. Именно здесь интеграционные тесты начинают становиться паршивыми, в то время как изолированные тесты остаются быстрыми и управляемыми

    Чтобы быть более конкретным, что произойдет, если один из методов вашего адаптера HTable возвращает пустой список? Что если он вернет ноль? Что делать, если выдается исключение соединения? В контракте Адаптера должно быть определено, может ли произойти что-либо из этого, и любой из его потребителей должен быть готов справиться с этими ситуациями , следовательно, необходимо провести их тестирование.

Подводя итог: вы не увидите никакого снижения качества, удалив ложные тесты, если они проверяли те же вещи, что и ваши интеграционные тесты . Однако попытка представить дополнительные изолированные тесты (и контрактные тесты ) может помочь вам обдумать ваши интерфейсы / контракты и повысить качество за счет устранения дефектов, о которых было бы сложно думать и / или медленных тестировать с помощью интеграционных тестов.

guillaume31
источник
+1 Я считаю, что с помощью фиктивных тестов гораздо проще создавать краевые случаи, чем заполнять базу данных этими случаями.
Роб
Я согласен с большей частью вашего ответа. Тем не менее, я не уверен, что я согласен с адаптером. HTable - это издевка, потому что она довольно голая. Например, если вы хотите выполнить пакетную операцию get, вы должны создать группу объектов Get, поместить их в список, а затем вызвать HTable.batch (). С точки зрения насмешек это серьезная проблема, потому что вам нужно создать собственный Matcher, который проверяет список объектов get, которые вы передаете HTable.batch (), а затем возвращает правильные результаты для этого списка объектов get (). СЕРЬЕЗНАЯ боль.
sangfroid
Я полагаю, я мог бы создать хороший, дружественный класс-обертку для HTable, который позаботился бы обо всем этом домашнем хозяйстве, но в этот момент ... я чувствую, что я вроде как создаю структуру вокруг HTable, и это действительно должно быть моей работой? Обычно "Давайте построим фреймворк!" это знак, что я иду в неправильном направлении. Я мог бы потратить дни на написание уроков, чтобы сделать HBase более дружелюбным, и я не знаю, насколько это полезно для меня. Кроме того, тогда я остановлюсь на интерфейсе или оболочке, а не на простом старом объекте HTable, и это определенно сделает мой код более сложным.
sangfroid
Но я согласен с вашей главной идеей, что нельзя писать издевательства для классов, которые им не принадлежат. И определенно согласен, что нет смысла писать фиктивный тест, который тестирует то же самое, что и интеграционный тест. Казалось бы, макеты лучше всего подходят для тестирования интерфейсов / контрактов. Спасибо за совет - это мне очень помогло!
sangfroid
Я мало представляю, что на самом деле делает HTable и как вы его используете, так что не берите мой пример с оберткой в ​​письмо. Я упомянул обертку / адаптер, потому что я думал, что вещь, чтобы обернуть, была относительно маленькой. Вам не нужно вводить реплику «один-к-одному» в HTable, что, конечно, будет болезненно, не говоря уже о целой среде, но вам нужен шов , интерфейс между областью вашего приложения и областью HTable. Он должен перефразировать некоторые функциональные возможности HTable в собственные условия вашего приложения. Шаблон Repository - идеальное воплощение такого шва, когда дело доходит до доступа к данным.
guillaume31
11

С философской точки зрения, тесты, использующие макеты, должны иметь приоритет над тестами, использующими конечную точку

Я думаю , по крайней мере, это точка нынешнего продолжающегося спора среди TDD сторонников.

Мое личное мнение выходит за рамки того, чтобы сказать, что тест на основе имитации - это в основном способ представления формы интерфейса контракта ; в идеале он ломается (то есть выходит из строя) тогда и только тогда, когда вы меняете интерфейс . И как таковой, в достаточно строго типизированных языках, таких как Java, и при использовании явно определенного интерфейса это почти полностью излишне: компилятор уже сообщит вам, если вы изменили интерфейс.

Основным исключением является случай, когда вы используете очень общий интерфейс, возможно, основанный на аннотациях или отражениях, что компилятор не может автоматически использовать автоматическое отслеживание. Даже в этом случае вы должны проверить, есть ли способ сделать проверку программно (например, библиотеку проверки синтаксиса SQL), а не вручную, используя макеты.

Это тот последний случай, который вы делаете при тестировании с «живой» локальной базой данных; Реализация htable вступает в силу и применяет гораздо более полную проверку контракта interfacve, чем вы когда-либо могли бы написать вручную.

К сожалению, гораздо более распространенным применением имитационного тестирования является тест, который:

  • проходит за любой код был на момент написания теста
  • не дает никаких гарантий о каких-либо свойствах кода, кроме того, что он существует и работает
  • не работает каждый раз, когда вы меняете этот код

Такие тесты, конечно, должны быть удалены на месте.

Сору
источник
1
Я не могу поддержать это достаточно. Я бы предпочел иметь покрытие в 1% с хорошими тестами, чем покрытие наполнителя в 100%.
Ян
3
Тесты, основанные на имитации, действительно описывают контракт, используемый двумя объектами для общения, но они выходят далеко за рамки возможностей системы типов языка, такого как Java. Речь идет не только о сигнатурах методов, они также могут указывать допустимые диапазоны значений для аргументов или возвращаемых результатов, какие исключения допустимы, в каком порядке и сколько раз можно вызывать методы и т. Д. Один только компилятор не предупредит вас, если есть изменения в тех. В этом смысле я не думаю, что они излишни. Смотрите infoq.com/presentations/integration-tests-scam для получения дополнительной информации о тестах на основе имитаций .
guillaume31
1
... согласен, т.е. тестирую логику интерфейса вызова
Rob
1
Безусловно, можно добавить непроверенные исключения, необъявленные предварительные условия и неявное состояние к списку вещей, которые делают интерфейс менее статически типизированным, и, таким образом, оправдывают тестирование на основе имитаций вместо простой компиляции. Проблема, однако, заключается в том , что , когда эти аспекты делают изменения, их спецификация неявная и распространяются среди тестов всех клиентов. Которые, скорее всего, не будут обновлены, и поэтому молча прячутся за зеленой галочкой.
сору
«их спецификация неявна»: нет, если вы пишете контрактные тесты для своих интерфейсов ( blog.thecodewhisperer.com/2011/07/07/contract-tests-an-example ) и придерживаетесь их при настройке макетов.
guillaume31
5

Сколько дольше выполняется тест на основе конечных точек, чем тест на основе имитации? Если он значительно длиннее, то да, стоит потратить время на написание тестов, чтобы ускорить модульные тесты - потому что вам придется запускать их много, много раз. Если это не значительно дольше, даже если тесты на основе конечных точек не являются «чистыми» модульными тестами, если они хорошо выполняют тестирование модуля, нет никаких оснований быть религиозными в этом отношении.

Карл Манастер
источник
4

Я полностью согласен с ответом guillaume31, никогда не издевайтесь над типами, которые вам не принадлежат!

Обычно боль в тесте (издевательство над сложным интерфейсом) отражает проблему в вашем дизайне. Возможно, вам нужна некоторая абстракция между вашей моделью и кодом доступа к данным, например, использование шестиугольной архитектуры и шаблона репозитория. Это наиболее обычный способ решения подобных проблем.

Если вы хотите сделать интеграционный тест для проверки, сделайте интеграционный тест, если вы хотите выполнить модульный тест, потому что вы тестируете свою логику, выполните модульный тест и изолируйте постоянство. Но, выполняя интеграционный тест, потому что вы не знаете, как изолировать свою логику от внешней системы (или потому что изолировать ее от боли) - это большой запах, вы выбираете интеграцию, а не единицу, для ограничения вашего дизайна, а не для реальной необходимости. проверить интеграцию.

Взгляните на эту форму разговора Иана Купера: http://vimeo.com/68375232 , он рассказывает о гексагональной архитектуре и тестировании, он рассказывает о том, когда и что надо издеваться, действительно вдохновенный доклад, который решает многие вопросы, такие как ваш, о реальном TDD ,

AlfredoCasado
источник
1

TL; DR То, как я это вижу, зависит от того, сколько усилий вы потратите на тесты, и было бы лучше потратить больше на вашу реальную систему.

Длинная версия:

Здесь есть несколько хороших ответов, но я думаю по-другому: тестирование - это экономическая деятельность, которая должна окупаться, и если время, которое вы тратите, не возвращается на разработку и надежность системы (или что-то еще, чего вы хотите добиться тесты) тогда вы можете делать плохие инвестиции; Вы занимаетесь созданием систем, а не написанием тестов. Поэтому сокращение усилий по написанию и сопровождению тестов имеет решающее значение.

Например, некоторые основные значения, которые я получаю из тестов:

  • Надежность (и, следовательно, скорость разработки): рефакторинг кода / интеграция новой инфраструктуры / переключение компонента / порта на другую платформу, уверенность в том, что все еще работает
  • Обратная связь по дизайну: классическая обратная связь TDD / BDD «используй свой код» на интерфейсах низкого / среднего уровня

Тестирование на работающей конечной точке все еще должно обеспечить это.

Некоторые недостатки при тестировании с живой конечной точкой:

  • Настройка среды - настройка и стандартизация среды выполнения теста - это более трудоемкий процесс, и слегка отличающиеся настройки среды могут привести к слегка различному поведению
  • Безгражданство - работа с работающей конечной точкой может в конечном итоге способствовать написанию тестов, которые основаны на переходящем состоянии конечной точки, которое является хрупким и трудным для рассуждения (т. Е. Когда что-то не получается, происходит ли сбой из-за странного состояния?)
  • Среда выполнения теста хрупка - если тест не пройден, это тест, код или действующая конечная точка?
  • Скорость бега - живая конечная точка обычно медленнее, а иногда ее сложнее распараллелить
  • Создание крайних случаев для тестирования - обычно тривиально с имитацией, иногда - болью с живой конечной точкой (например, сложными для установки являются ошибки транспорта / HTTP)

Если бы я находился в такой ситуации, и недостатки, по-видимому, не были проблемой, в то время как насмешка над конечной точкой значительно замедлила мою тестовую запись, я бы проверил живую конечную точку в одно мгновение, если бы был уверен проверьте еще раз через некоторое время, чтобы увидеть, что недостатки не являются проблемой на практике.

orip
источник
1

С точки зрения тестирования есть некоторые требования, которые абсолютно необходимы:

  • Тестирование (единичное или иное) никогда не должно касаться производственных данных
  • Результаты одного теста никогда не должны влиять на результаты другого теста
  • Вы всегда должны начинать с известной позиции

Это большая проблема при подключении к любому источнику, который поддерживает состояние вне ваших тестов. Это не «чистый» TDD, но команда Ruby on Rails решила эту проблему таким образом, чтобы ее можно было адаптировать для ваших целей. Тестовая структура рельсов работала так:

  • Тестовая конфигурация была автоматически выбрана при запуске юнит-тестов
  • База данных была создана и инициализирована в начале выполнения модульных тестов.
  • База данных была удалена после запуска модульных тестов
  • Если используется SqlLite, в тестовой конфигурации используется база данных RAM

Вся эта работа была встроена в тестовую оснастку, и она работает достаточно хорошо. Это еще не все, но для этого разговора достаточно основ.

В различных командах, с которыми я работал со временем, мы делали выбор, который способствовал бы тестированию кода даже если это был не самый правильный путь. В идеале мы должны обернуть все вызовы в хранилище данных кодом, который мы контролируем. Теоретически, если какой-либо из этих старых проектов получит новое финансирование, мы можем вернуться и перевести их из базы данных в Hadoop, сосредоточив наше внимание только на нескольких классах.

Важные аспекты - не связываться с производственными данными и убедиться, что вы действительно тестируете то, что, по вашему мнению, тестируете. Очень важно иметь возможность сбрасывать внешний сервис до известного базового уровня по требованию - даже из вашего кода.

Берин Лорич
источник