Подтвердить что ты не робот

Единичные тесты. Преимущества модульных испытаний с изменениями контракта?

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

Возможно, кто-нибудь может рассказать мне, как подойти к этой проблеме. Позвольте мне уточнить:

Итак, давайте скажем, что есть класс, который выполняет некоторые отличные вычисления. В контракте говорится, что он должен вычислить число или он возвращает -1, когда по какой-то причине он не работает.

У меня есть контрактные тесты, которые проверяют это. И во всех моих других тестах я заглушу этот отличный калькулятор.

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

Мои тесты контрактов не пройдут, и я их исправлю. Но все мои обманутые объекты будут по-прежнему использовать старые правила контракта. Эти тесты будут успешными, в то время как они не должны!

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

Как вы думаете об этом случае? Я никогда не думал об этом. По-моему, эти изменения в модульных тестах были бы приемлемыми. Если я не буду использовать модульные тесты, я бы также заметил, что такие ошибки возникают на этапе тестирования (тестировщиками). Однако я недостаточно уверен, чтобы указать, что будет стоить больше времени (или меньше).

Любые мысли?

4b9b3361

Ответ 1

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

Некоторые простые вещи, которые вызывают такую ​​хрупкость:

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

Дизайн тестирования - важная проблема, которая часто игнорируется новичками TDD. Это часто приводит к хрупким испытаниям, которые затем заставляют новичков отклонять TDD как "непродуктивные".

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

Тесты должны быть организованы следующим образом:

  • Модульные тесты обеспечивают почти 100% охват кода. Они тестируют независимые устройства. Они написаны программистами, использующими язык программирования системы.
  • Тестирование компонентов охватывает ~ 50% системы. Они написаны бизнес-аналитиками и QA. Они написаны на языке, таком как FitNesse, Selenium, Cucumber и т.д. Они тестируют целые компоненты, а не отдельные единицы. Они проверяют в первую очередь счастливые случаи пути и некоторые очень заметные случаи несчастного пути.
  • Тесты интеграции охватывают ~ 20% системы. Они тестируют небольшие сборки компонентов, в отличие от всей системы. Также написано в FitNesse/Selenium/Cucumber и т.д. Написано архитекторами.
  • Системные тесты охватывают ~ 10% системы. Они тестируют всю систему, объединенную вместе. Снова они написаны в FitNesse/Selenium/Cucumber и т.д. Написано архитекторами.
  • Экспериментальные ручные тесты. (См. Джеймс Бах) Эти тесты носят ручной характер, но не сценарий. Они используют человеческую изобретательность и творчество.

Ответ 2

Лучше исправить unit test, которые не выполняются из-за преднамеренных изменений кода, чем не имеют тестов, чтобы поймать ошибки, которые в конечном итоге внесены этими изменениями.

Когда ваша кодовая база имеет хороший охват unit test, вы можете столкнуться с многочисленными неудачами unit test, которые не связаны с ошибками в коде, но преднамеренными изменениями в контрактах или рефакторинге кода.

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

Ответ 3

Модульные тесты наверняка не могут поймать все ошибки, даже в идеальном случае 100% покрытия кода/функциональности. Думаю, этого не следует ожидать.

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

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

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

Ответ 4

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

  • Приемочные тесты - проверьте пару или несколько классов. Эти тесты обычно выстраиваются в пользовательские хранилища, которые должны быть реализованы, поэтому вы проверяете, что пользовательская история "работает". Им не нужно подключаться к БД или другим внешним системам, но может.
  • Интеграционные тесты - в основном для проверки подключения внешних систем и т.д.
  • Полные сквозные тесты - проверка всей системы.

Обратите внимание, что даже если у вас есть 100% unit test охват, вы даже не гарантируете, что ваше приложение запустится! Вот почему вам нужны тесты более высокого уровня. Существует так много разных уровней тестов, потому что чем ниже вы что-то тестируете, тем дешевле это обычно (с точки зрения разработки, поддержания инфраструктуры тестирования и времени выполнения).

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

Ответ 6

Одно из правил для кода модульных тестов (и всего другого кода, используемого для тестирования) - это обрабатывать его так же, как и производственный код - не более того, не менее - то же самое.

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

Вероятно, ваша стратегия тестирования должна включать в себя что-то для решения проблемы, которую вы описали в первоначальном post-something по строкам, определяя, какой тестовый код (включая заглушки /mocks ) должен быть пересмотрен (выполнен, проверен, изменен, исправлен и т.д.), когда дизайнер меняет функцию/метод в производственном коде. Поэтому стоимость изменения любого производственного кода должна включать в себя затраты на это - если нет - тестовый код станет "гражданином третьего сорта", и уверенность дизайнеров в наборе unit test, а также его значимость уменьшится. Очевидно, что ROI находится во времени обнаружения ошибок и исправления.

Ответ 7

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

Ответ 8

Я смотрю на это таким образом, когда ваш контракт меняется, вы должны рассматривать его как новый контракт. Поэтому для этого "нового" контракта вы должны создать совершенно новый набор UNIT test. Тот факт, что у вас есть уже существующий набор тестовых примеров, помимо точки.

Ответ 9

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

Короче

вместо того, чтобы говорить "return -1 для x == 0" или "throw CannotCalculateException для x == y", underspecify niftyCalcuatorThingy(x,y) с предварительным условием x!=y && x!=0 в соответствующих ситуациях (см. ниже). Таким образом, ваши заглушки могут вести себя произвольно для этих случаев, ваши модульные тесты должны отражать это, и вы обладаете максимальной модулярностью, то есть свободой произвольно изменять поведение вашей системы, находящейся под тестом, для всех недоказанных случаев - без необходимости менять контракты или тесты.

Обозначение, в случае необходимости

Вы можете отличить ваш оператор "-1, когда он по какой-то причине не работает", согласно следующим критериям: Является ли сценарий

  • исключительное поведение, которое может проверить реализация?
  • в домене метода/ответственности?
  • исключение, которое вызывающий (или кто-то ранее в стеке вызовов) может восстановить из /handle каким-либо другим способом?

Если и только если 1) - 3), укажите сценарий в контракте (например, что EmptyStackException вызывается при вызове pop() в пустом стеке).

Без 1) реализация не может гарантировать конкретное поведение в исключительном случае. Например, Object.equals() не указывает никакого поведения, когда условие рефлексивности, симметрии, транзитивности и согласованности не выполняется.

Без 2), SingleResponsibilityPrinciple не выполняется, модульность нарушена, и пользователи/читатели кода запутались. Например, Graph transform(Graph original) не должен указывать, что MissingResourceException может быть выбрано, потому что в глубине вниз выполняется клонирование через сериализацию.

Без 3) вызывающий не может использовать указанное поведение (определенное возвращаемое значение/исключение). Например, если JVM выбрасывает UnknownError.

Плюсы и минусы

Если вы указываете случаи, когда 1), 2) или 3) не выполняется, вы получаете некоторые трудности:

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

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

Как компромисс, мне нравится использовать следующую схему договора, где это возможно:

< (Semi-) формальное PRE- и POST-условие, включая исключительные поведение где 1) - 3). >

Если PRE не выполняется, текущая реализация выдает RTE A, B или С.