Делает ли TDD защитное программирование избыточным?

104

Сегодня у меня была интересная беседа с коллегой.

Я защитник программиста. Я считаю, что всегда должно соблюдаться правило « класс должен гарантировать, что его объекты имеют действительное состояние при взаимодействии с ним извне ». Причиной этого правила является то, что класс не знает, кто является его пользователями, и что он должен предсказуемо потерпеть неудачу, когда с ним незаконно взаимодействуют. На мой взгляд, это правило распространяется на все классы.

В конкретной ситуации, в которой у меня было сегодняшнее обсуждение, я написал код, который проверяет правильность аргументов моего конструктора (например, целочисленный параметр должен быть> 0), и если предварительное условие не выполняется, генерируется исключение. Мой коллега, с другой стороны, считает, что такая проверка избыточна, потому что модульные тесты должны выявлять любые неправильные использования класса. Кроме того, он считает, что проверки защитного программирования должны также подвергаться модульному тестированию, поэтому защитное программирование добавляет много работы и поэтому не является оптимальным для TDD.

Правда ли, что TDD способен заменить защитное программирование? Является ли проверка параметров (и я не имею в виду ввод пользователя) ненужной в результате? Или эти две техники дополняют друг друга?

user2180613
источник
120
Вы передаете свою полностью протестированную модулем библиотеку без проверок конструктора клиенту для использования, и они нарушают контракт класса. Что хорошего в этих юнит-тестах сейчас?
Роберт Харви
42
ИМО, это наоборот. Защитное программирование, правильные предварительные и про-условия, а также система с богатым типом делают тесты излишними.
садовод
37
Могу ли я опубликовать ответ, который просто говорит "Хорошее горе?" Защитное программирование защищает систему во время выполнения. Тесты проверяют все потенциальные условия времени выполнения, о которых может подумать тестировщик, включая недопустимые аргументы, передаваемые конструкторам и другим методам. Тесты, если они завершены, подтвердят, что поведение во время выполнения будет соответствовать ожидаемым, в том числе будут выданы соответствующие исключения или другое преднамеренное поведение, возникающее при передаче неверных аргументов. Но тесты не делают ничего страшного, чтобы защитить систему во время выполнения.
Крейг,
16
«модульные тесты должны отлавливать любые неправильные использования класса» - ну как? Модульные тесты покажут вам поведение при заданных правильных аргументах и ​​при неправильных аргументах; они не могут показать вам все аргументы, которые ему когда-либо будут предоставлены.
OJFord
34
Я не думаю, что видел лучший пример того, как догматическое мышление о разработке программного обеспечения может привести к вредным выводам.
Сденхэм

Ответы:

196

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

Никакая методология не может заставить пользователей правильно использовать код.

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

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

Или эти две техники дополняют друг друга?

TDD разработает тесты. Реализация проверки параметров заставит их пройти.

enderland
источник
7
Я не согласен с мнением о том, что проверка предварительных условий должна быть проверена, но я не согласен с мнением моего коллеги о том, что дополнительная работа, вызванная необходимостью проверки проверки предварительных условий, является аргументом, чтобы не создавать проверку предварительных условий в первой место. Я отредактировал свой пост, чтобы уточнить.
user2180613 23.09.16
20
@ user2180613 Создайте тест, который проверяет, что сбой предварительного условия обрабатывается надлежащим образом: теперь добавление проверки - это не «лишняя» работа, а работа, которая требуется TDD, чтобы сделать тест зеленым. Если ваш коллега считает, что вы должны выполнить тест, наблюдать, как он проваливается, а затем и только тогда осуществлять проверку предварительных условий, то он может иметь точку зрения с точки зрения TDD-пуриста. Если он говорит просто игнорировать чек, значит, он глупый. В TDD нет ничего, что говорило бы о том, что вы не можете быть активными в написании тестов для возможных сбоев.
RM
4
@RM Вы не пишете тест, чтобы проверить предварительную проверку. Вы пишете тест для проверки ожидаемого правильного поведения вызываемого кода. Проверка предварительных условий, с точки зрения теста, является непрозрачной деталью реализации, которая обеспечивает правильное поведение. Если вы думаете о лучшем способе обеспечить правильное состояние в вызываемом коде, сделайте это таким образом, вместо традиционной проверки предусловий. Тест будет подкрепляет , были ли вы успешны, и до сих пор не знает , или все равно , как вы это сделали.
Крэйг,
@ user2180613 Это прекрасное оправдание: D если ваша цель при написании программного обеспечения - сократить количество тестов, которые вам нужно написать и запустить, не пишите никакого программного обеспечения - ноль тестов!
Gusdor
3
Это последнее предложение этого ответа прибивает его.
Роберт Грант
32

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

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

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

Кевин Фе
источник
Правильно ли говорить, что валидация параметров является формой валидации предварительных условий, а модульные тесты являются валидациями постусловий, поэтому они дополняют друг друга?
user2180613 23.09.16
1
«Это то же самое, что и тестирование любого другого кода, вы хотите убедиться, что все операции, даже недействительные, имеют ожидаемый результат». Этот. Никакой код никогда не должен проходить, когда передается ввод, который он не предназначен для обработки. Это нарушает принцип «быстро провал», и это может сделать отладку кошмаром.
jpmc26
@ user2180613 - не совсем, но более того, модульные тесты проверяют условия отказа, которых ожидает разработчик, в то время как методы защитного программирования проверяют условия, которых разработчик не ожидает. Модульные тесты могут использоваться для проверки предварительных условий (с помощью фиктивного объекта, введенного вызывающей стороне, которая проверяет предварительное условие).
Периата Breatta
1
@ jpmc26 Да, неудача - это «ожидаемый результат» теста. Вы тестируете, чтобы показать, что он терпит неудачу, вместо того, чтобы молча демонстрировать какое-то неопределенное (неожиданное) поведение.
KRyan
6
TDD ловит ошибки в вашем собственном коде, защитное программирование ловит ошибки в коде других людей. Таким образом, TDD может помочь вам обеспечить достаточную оборону :)
16:00
30

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

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

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

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

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

Амон
источник
16

Есть тесты для поддержки и обеспечения защитного программирования

Защитное программирование защищает целостность системы во время выполнения.

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

РЕДАКТИРОВАТЬ: аналогия

А как насчет аналогии с комментариями в коде?

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

Допустим, вы вложили в тесты много внутренних знаний о вашей кодовой базе, например, MethodA не может принимать значение NULL, а аргумент MethodB должен быть > 0. Затем код меняется. Нуль в порядке для A сейчас, и B может принимать значения, такие как -10. Существующие тесты теперь функционально неверны, но будут продолжать проходить.

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

Тесты проверяют поведение системы. Это фактическое поведение присуще самой системе, а не присуще тестам.

Что возможно могло пойти не так?

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

Что означает, что оборонительное программирование - это главное .

TDD управляет защитным программированием, если тесты комплексные.

Больше тестов, вождение более оборонительного программирования

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

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

Вообще говоря ...

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

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

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

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

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

Особенно, если программисты этой другой системы не защитили код.

Craig
источник
2
Забавно, но понижение произошло так быстро, что теперь абсолютно возможно, что даунготер мог прочитать за пределами первого абзаца.
Крейг,
1
:-) Я только что проголосовал, не читая дальше первого абзаца, так что, надеюсь, это уравновесит его ...
SusanW
1
Показалось что я мог сделать :-) ( На самом деле, я даже читать остальные просто чтобы убедиться , что не должны быть неаккуратным -.! Особенно на тему , как это)
SusanW
1
Я подумал, что вы, вероятно, имели. :)
Крейг,
Защитные проверки могут быть выполнены во время компиляции с помощью таких инструментов, как Code Contracts.
Мэтью Уайтед
9

Вместо TDD давайте поговорим о «тестировании программного обеспечения» в целом, а вместо «защитного программирования» в целом, давайте поговорим о моем любимом способе выполнения защитного программирования, которое заключается в использовании утверждений.


Итак, поскольку мы проводим тестирование программного обеспечения, нам следует прекратить помещать операторы assert в производственный код, верно? Позвольте мне сосчитать, каким образом это неправильно:

  1. Утверждения не являются обязательными, поэтому, если они вам не нравятся, просто запустите вашу систему с отключенными утверждениями.

  2. Утверждения проверяют то, что тестирование не может (и не должно). Потому что тестирование должно иметь представление «черного ящика» вашей системы, в то время как утверждения имеют представление «белого ящика». (Конечно, так как они живут в нем.)

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

  4. Утверждения могут отлавливать ошибки в коде тестирования. Сталкивались ли вы когда-нибудь с ситуацией, когда тест не пройден, и вы не знаете, кто не прав - рабочий код или тест?

  5. Утверждения могут быть более уместными, чем тестирование. Тесты будут проверять то, что предписано функциональными требованиями, но в коде часто приходится делать определенные предположения, которые являются гораздо более техническими, чем это. Люди, которые пишут документы о функциональных требованиях, редко думают о делении на ноль.

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

  7. Утверждения уменьшают сложность программы. Каждая строка кода, которую вы пишете, увеличивает сложность программы. Утверждения и ключевое слово final( readonly) - единственные известные мне две конструкции, которые на самом деле уменьшают сложность программы. Это бесценно.

  8. Утверждения помогают компилятору лучше понять ваш код. Пожалуйста, попробуйте это дома: void foo( Object x ) { assert x != null; if( x == null ) { } }ваш компилятор должен выдать предупреждение о том, что условие x == nullвсегда ложно. Это может быть очень полезно.

Выше было резюме поста из моего блога, 2014-09-21 "Утверждения и тестирование"

Майк Накис
источник
Я думаю, что я в основном не согласен с этим ответом. (5) В TDD тестовый набор является спецификацией. Вы должны написать самый простой код для прохождения тестов, не более того. (4) Красно-зеленый рабочий процесс гарантирует, что тест не пройден, когда он должен, и пройден, когда намечена функциональность. Утверждения не очень помогают здесь. (3,7) Документация - это документация, утверждения - нет. Но, сделав предположения явными, код становится более самодокументированным. Я думаю о них как о исполняемых комментариях. (2) Тестирование белого ящика может быть частью действующей стратегии тестирования.
Амон
5
«В TDD набор тестов является спецификацией. Вы должны писать самый простой код, который позволяет проходить тесты, не более того». Я не думаю, что это всегда хорошая идея. Как указано в ответе, есть дополнительное внутреннее предположение в коде, которое можно проверить. Как насчет внутренних ошибок, которые компенсируют друг друга? Ваши тесты пройдены, но некоторые предположения в вашем коде неверны, что может привести к коварным ошибкам позже.
Джорджио
5

Я полагаю, что в большинстве ответов отсутствует критическое различие: это зависит от того, как будет использоваться ваш код.

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

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

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

JacquesB
источник
4
Если вызываемая процедура не проверяет достоверность входных данных (что является первоначальной дискуссией), то ваши модульные тесты не могут гарантировать, что рассматриваемый модуль вызывается только с допустимым вводом. В частности, он может быть вызван с неверным вводом, но в любом случае может случиться так, что он в любом случае возвращает правильный результат - существуют различные типы неопределенного поведения, обработки переполнения и т. Д., Которые могут вернуть ожидаемый результат в среде тестирования с отключенными оптимизациями, но сбой в производстве.
Петерис
@Peteris: Вы думаете о неопределенном поведении как в C? Вызов неопределенного поведения, которое имеет разные результаты в разных средах, очевидно, является ошибкой, но это также не может быть предотвращено проверками предварительных условий. Например, как вы проверяете аргумент указателя, указывающий на действительную память?
JacquesB
3
Это будет работать только в самых маленьких магазинах. Как только ваша команда выйдет, скажем, из шести человек, вам все равно понадобятся проверочные проверки.
Роберт Харви
1
@RobertHarvey: В этом случае система должна быть разделена на подсистемы с четко определенными интерфейсами, и на интерфейсе должна быть выполнена проверка ввода.
JacquesB
это. Это зависит от кода, этот код будет использоваться командой? Есть ли у команды доступ к исходному коду? Если его чисто внутренний код, то проверка на аргументы может быть просто обременением, например, вы проверяете на 0, затем выбрасываете исключение, и вызывающая сторона затем просматривает код: «Этот класс может выдать исключение и т. Д. И т. Д.» И ждать… в этом случае, что объект никогда не получит 0, так как они были отфильтрованы 2 уровня раньше. Если это код библиотеки, который будет использоваться третьими лицами, то это уже другая история. Не весь код написан для использования всем миром.
Александр Фуляр
3

Этот аргумент немного сбивает меня с толку, потому что когда я начал практиковать TDD, мои модульные тесты вида «объект реагирует <определенным образом>, когда <недопустимый вход>» увеличился в 2 или 3 раза. Мне интересно, как вашему коллеге удается успешно проходить подобные юнит-тесты без проверки его функций.

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

Другими словами, то, что делает TDD, не мешает вам нуждаться в проверочном коде, а помогает вам избежать его забвения .

Карл Билефельдт
источник
2

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

Мне кажется, что аргумент таков:

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

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

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

Имейте в виду, однако, что если вы задокументируете и протестируете поведение вашей функции при передаче значения <= 0, то отрицательные значения больше не будут недействительными (по крайней мере, не более недействительными, чем любой аргумент вообще throw, поскольку тоже задокументировано, чтобы выбросить исключение!). Абоненты имеют право полагаться на это защитное поведение. Если позволяет язык, возможно, это в любом случае лучший сценарий - функция не имеет «недопустимых входных данных», но вызывающие абоненты, которые ожидают, что функция не вызовет исключение, должны быть достаточно проверены модулем, чтобы убедиться, что они не передать любые значения, которые вызывают это.

Несмотря на то, что вы думаете, что ваш коллега несколько менее прав, чем большинство ответов, я прихожу к одному и тому же выводу: оба метода дополняют друг друга. Программируйте защиту, документируйте свои защитные проверки и проверяйте их. Работа является «ненужной», только если пользователи вашего кода не могут воспользоваться полезными сообщениями об ошибках, когда они делают ошибки. Теоретически, если они тщательно протестируют весь свой код перед интеграцией с вашим, и в их тестах никогда не будет ошибок, то они никогда не увидят сообщений об ошибках. На практике, даже если они используют TDD и внедрение полной зависимости, они все равно могут исследовать их во время разработки или в тестировании может быть ошибка. В результате они вызывают ваш код до того, как их код станет идеальным!

Стив Джессоп
источник
Эта задача сделать акцент на тестировании вызывающих, чтобы убедиться, что они не передают неверные значения, похоже, поддается хрупкому коду с большим количеством басовых зависимостей и отсутствием четкого разделения проблем. Я действительно не думаю, что мне понравился бы код, который возник бы в результате размышлений об этом подходе.
Крейг
@Craig: посмотрите на это с другой стороны, если вы изолировали компонент для тестирования путем имитации его зависимостей, то почему бы вам не проверить, что он передает только правильные значения этим зависимостям? А если вы не можете изолировать компонент, разве вы разделяете проблемы? Я не согласен с защитным кодированием, но если защитные проверки являются средством проверки правильности вызова кода, то это беспорядок. Так что я думаю, что коллега спрашивающего прав, что чеки излишни, но неправильно воспринимать это как причину, чтобы не писать их :-)
Стив Джессоп
я вижу только одну явную дыру в том, что я до сих пор только проверяю, что мои собственные компоненты не могут передавать недопустимые значения этим зависимостям, что я полностью согласен с тем, что должно быть сделано, но сколько решений требуется от того, сколько бизнес-менеджеров делает частную компонент публичный, чтобы партнеры могли это назвать? Это на самом деле напоминает мне о дизайне базы данных и обо всех текущих любовных связях с ORM, в результате чего многие (в основном молодые) люди заявляют, что базы данных - просто тупое сетевое хранилище и не должны защищать себя с помощью ограничений, внешних ключей и хранимых процедур.
Крейг
Другая вещь, которую я вижу, состоит в том, что в этом сценарии, конечно, вы тестируете вызовы только для имитаций, а не для реальных зависимостей. В конечном счете, это код в этих зависимостях, который может или не может работать с определенным переданным значением, а не код в вызывающей стороне. Таким образом, зависимость должна делать правильные вещи, и должно быть достаточное независимое тестовое покрытие зависимости, чтобы убедиться, что она работает. Помните, что эти тесты, о которых мы говорим, называются «модульными» тестами. Каждая зависимость - это единица. :)
Крейг,
1

Публичные интерфейсы могут и будут использоваться неправильно

Заявление вашего коллеги «модульные тесты должны отлавливать любые неправильные использования класса» строго ложно для любого интерфейса, который не является закрытым. Если открытая функция может быть вызвана с целочисленными аргументами, то она может и будет вызываться с любыми целочисленными аргументами, и код должен вести себя соответствующим образом. Если сигнатура общедоступной функции принимает, например, тип Java Double, тогда все возможные значения - null, NaN, MAX_VALUE, -Inf. Ваши модульные тесты не могут отловить неправильное использование класса, потому что эти тесты не могут тестировать код, который будет использовать этот класс, потому что этот код еще не написан, может быть не написан вами и определенно выйдет за рамки ваших модульных тестов. ,

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

Петерис
источник
Заголовок и первый абзац этого утверждения верны, потому что не модульные тесты будут выполнять код во время выполнения. Это любой другой код времени выполнения и изменение реальных условий, некорректные пользовательские вводы и попытки взлома взаимодействуют с кодом.
Крейг,
1

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

Функция требует тестирования: действительно ли работает защита от неправильного использования? Цель тестирования этой функции - попытаться показать, что это не так: придумать какое-то неправильное использование модуля, которое не будет проверено его проверками.

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

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

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

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

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

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

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

TL; Д.Р .: Ваш коллега, вероятно, не идиот; вы просто обсуждаете одну и ту же вещь с разных точек зрения, потому что требования не полностью собраны, и у каждого из вас есть свое представление о том, что такое «неписаные требования». Вы думаете, что когда нет особых требований к проверке параметров, вам все равно следует написать подробную проверку; коллега думает, просто дайте надёжному коду нижнего уровня взорваться, если параметры неверны. Спорить о неписаных требованиях через код несколько непродуктивно: признайте, что вы не согласны с требованиями, а не с кодом. Ваш способ кодирования отражает ваши требования; путь коллеги отражает его взгляд на требования. Если вы видите это таким образом, ясно, что правильно или неправильно не т в самом коде; код является всего лишь прокси для вашего мнения о том, какой должна быть спецификация.

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

Тесты определяют контракт вашего класса.

Как следствие, отсутствие теста определяет контракт, который включает в себя неопределенное поведение . Поэтому, когда вы переходите nullк Foo::Frobnicate(Widget widget)невидимому хаосу во время выполнения, вы все еще находитесь в рамках контракта с вашим классом.

Позже вы решаете: «Мы не хотим возможности неопределенного поведения», что является разумным выбором. Это означает, что вы должны иметь ожидаемое поведение для перехода nullк Foo::Frobnicate(Widget widget).

И вы документируете это решение, включив

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}
Caleth
источник
1

Хороший набор тестов будет использовать внешний интерфейс вашего класса и гарантировать, что такие неправильные действия приводят к правильному ответу (исключение или то, что вы определяете как «правильный»). Фактически, первый тестовый пример, который я пишу для класса, - это вызов его конструктора с аргументами вне допустимого диапазона.

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

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

Тоби Спейт
источник
0

Тесты TDD будут ловить ошибки при разработке кода .

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

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


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

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

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

Черный ястреб
источник
1
Я не отрицал, но я согласен с отрицательными предположениями о том, что добавление тонких различий к этому виду аргумента мутит воду.
Крейг,
@Craig Мне было бы интересно ваше мнение о конкретном примере, который я добавил.
Блэкхок
Мне нравится специфика примера. Единственное, что меня беспокоит, это общий аргумент. Например; вместе с ним приходит новый разработчик в команду и пишет новый компонент, который использует этот финансовый модуль. Новый парень не знает всех тонкостей системы, не говоря уже о том, что всевозможные экспертные знания о том, как система должна работать, встроены в тесты, а не в тестируемый код.
Крейг
Таким образом, новый парень / галстук пропускает создание некоторых жизненно важных тестов, и в результате вы получаете избыточность в своих тестах - тесты в разных частях системы проверяют одни и те же условия и с течением времени становятся непоследовательными, вместо того, чтобы просто ставить соответствующие утверждения и проверка предварительных условий в коде, где находится действие.
Крейг
1
Что-то вроде того. За исключением того, что многие аргументы здесь были о том, чтобы тесты для вызывающего кода делали все проверки. Но если у вас есть какая-то степень фанатизма, вы в конечном итоге делаете одни и те же проверки из разных мест, и это само по себе является проблемой обслуживания. Что если диапазон допустимых входных данных для процедуры изменится, но у вас есть знания предметной области для этого диапазона, встроенные в тесты, в которых используются различные компоненты? Я до сих пор полностью поддерживаю защитное программирование и использую профилирование, чтобы определить, есть ли у вас проблемы с производительностью для решения.
Крейг,
0

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

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

uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 

Теперь, если вы просто сделаете это, *(sum) = a + bэто будет работать, но только с некоторыми входами. a = 1и b = 2сделал бы sum = 3; однако, потому что размер суммы является байтом, a = 100и b = 200сделал бы sum = 44из-за переполнения. В C вы бы вернули ошибку в этом случае, чтобы показать, что функция не выполнена; исключение - это то же самое в вашем коде. Без учета сбоев или тестирования того, как их обработать, не будет работать долгое время, потому что, если эти условия возникают, они не будут обработаны и могут вызвать любое количество проблем.

Дом
источник
Это похоже на хороший пример с вопросом об интервью (почему у него есть возвращаемое значение и параметр «out» - и что происходит, когда sumуказатель NULL?).
Тоби Спейт