Типы юнит-тестов на основе полезности

13

С точки зрения ценности я вижу две группы юнит-тестов в моей практике:

  1. Тесты, которые проверяют некоторую нетривиальную логику. Написание их (до или после реализации) выявляет некоторые проблемы / потенциальные ошибки и помогает быть уверенным в том случае, если логика изменится в будущем.
  2. Тесты, которые проверяют очень тривиальную логику. Эти тесты больше похожи на код документа (обычно с макетами), чем тестируют его. Рабочий процесс тех тестов заключается не в том, что «какая-то логика изменилась, тест стал красным - слава Богу, я написал этот тест», но «изменился некоторый тривиальный код, тест стал ложноотрицательным - я должен поддерживать (переписывать) тест, не получая никакой прибыли» , Большую часть времени эти тесты не стоит поддерживать (за исключением религиозных соображений). И, согласно моему опыту во многих системах, эти тесты составляют 80% всех тестов.

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

SiberianGuy
источник
3
Я сохраняю проверку модульных тестов на наличие известных (специфических) ошибок, которые когда-то проскальзывали через исходный набор модульных тестов, как отдельную группу, роль которой заключается в предотвращении ошибок регрессии.
Конрад Моравский
6
Эти вторые виды испытаний - это то, что я рассматриваю как своего рода «изменение трения». Не сбрасывайте со счетов их полезность. Изменение даже тривиальности кода, как правило, имеет волновые эффекты во всей кодовой базе, и введение такого рода трений действует как препятствие для ваших разработчиков, так что они изменяют только те вещи, которые действительно в этом нуждаются, а не на основе каких-то причудливых или личных предпочтений.
Теластин
3
@Telastyn - Все, что касается твоего комментария, кажется мне совершенно безумным. Кто намеренно затруднит изменение кода? Зачем отговаривать разработчиков изменять код по своему усмотрению - вы им не доверяете? Они плохие разработчики?
Бенджамин Ходжсон
2
В любом случае, если изменение кода имеет тенденцию вызывать "волновые эффекты", тогда у вашего кода есть проблема дизайна - в этом случае разработчикам следует рекомендовать рефакторинг настолько, насколько это разумно. Хрупкие тесты активно препятствуют рефакторингу (тест не пройден; кто может быть обеспокоен, чтобы выяснить, был ли этот тест одним из 80% тестов, которые на самом деле ничего не делают? Вы просто нашли другой, более сложный способ сделать это). Но вы, похоже, рассматриваете это как желательную характеристику ... Я совсем не понимаю.
Бенджамин Ходжсон
2
В любом случае, OP может найти этот пост от создателя Rails интересным. Чтобы сильно упростить его точку зрения, вы, вероятно, должны попытаться отбросить эти 80% тестов.
Бенджамин Ходжсон

Ответы:

14

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

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

Как только вы признаете, что не любите писать модульные тесты и хотите стать лучше, вы скоро столкнетесь с модными стилями тестирования или другими методологиями. Под тестовыми методологиями я имею в виду такие практики, как тестирование вначале или то, что делает Эндрю Бинсток из DrDobbs, то есть написание тестов вместе с кодом. У обоих есть свои плюсы и минусы, и я отказываюсь вдаваться в какие-либо субъективные подробности, потому что это вызовет пламенную войну. Если вас не смущает, какая методология программирования лучше, тогда, возможно, стиль тестирования подойдет. Стоит ли использовать TDD, BDD, тестирование на основе свойств? У JUnit есть продвинутые концепции, называемые Теориями, которые стирают грань между TDD и тестированием на основе свойств. Что использовать, когда?

tl; dr Легко ошибиться в тестировании, это невероятно самоуверенно, и я не верю, что какая-либо одна методология тестирования по своей сути лучше, если она используется усердно и профессионально в контексте, в котором они уместны. Кроме того, тестирование На мой взгляд, это расширение утверждений или проверок работоспособности, которые использовались для обеспечения быстрого и безотказного подхода к разработке, который теперь стал намного, намного проще.

Для субъективного мнения я предпочитаю писать «фазы» тестов, из-за отсутствия лучшей фразы. Я пишу модульные тесты, которые тестируют классы изолированно, используя при необходимости макеты. Возможно, они будут выполнены с помощью JUnit или чего-то подобного. Затем я пишу интеграционные или приемочные тесты, которые проводятся отдельно и обычно только несколько раз в день. Это ваш нетривиальный вариант использования. Я обычно использую BDD, так как приятно выражать функции на естественном языке, что JUnit не может легко предоставить.

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

[1] Коррупция Agile Эндрю Бинсток

[2] Ответ на ответы предыдущей статьи

[3] Реакция дяди Боба на проворство гибкости

[4] Реакция Роберта Майерса на коррупцию Agile

[5] Зачем беспокоиться о тестировании огурцов?

[6] Ты делаешь это неправильно

[7] Отойдите от инструментов

[8] Комментарий к «Римские цифры Ката с комментарием»

[9] Римские цифры Ката с комментариями

IAE
источник
1
Одно из моих дружеских утверждений было бы то, что если вы пишете тест для проверки функции ежегодного налогового калькулятора, то вы не пишете модульный тест. Это интеграционный тест. Ваш калькулятор должен быть разбит на довольно простые единицы выполнения, и ваши юнит-тесты затем проверяют эти единицы. Если один из этих блоков перестает функционировать должным образом (тест начинает проваливаться), то это как выбивание части фундаментной стены, и вам нужно исправить код (вообще не тест). Либо так, либо вы определили немного кода, который больше не нужен и должен быть отброшен.
Крейг,
1
@Craig: Точно! Это то, что я имел в виду, не зная, как писать правильные тесты. Будучи студентом колледжа, сборщик налогов был одним большим классом, написанным без должного понимания SOLID. Вы абсолютно правы, считая, что это скорее интеграционный тест, чем что-либо еще, но для нас это был неизвестный термин. Мы были только подвергнуты "модульным" тестам нашим профессором.
IAE
5

Я считаю, что важно иметь тесты обоих типов и использовать их там, где это необходимо.

Как вы сказали, есть две крайности, и я, честно говоря, не согласен ни с одной из них.

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

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

Я твердо верю в модульное тестирование и использую TDD везде, где это имеет смысл. Модульные тесты, безусловно, приносят пользу в виде повышенного качества и «быстрого отказа» при изменении кода. Однако следует помнить и старое правило 80/20 . В какой-то момент вы достигнете убывающей отдачи при написании тестов, и вам нужно перейти к более продуктивной работе, даже если есть некоторая измеримая ценность, которую нужно иметь при написании большего количества тестов.


источник
Написание теста, гарантирующего, что система отслеживает возраст человека, не является модульным тестом, IMO. Это интеграционный тест. Модульный тест будет проверять общую единицу выполнения (иначе говоря, «процедуру»), которая, скажем, вычисляет значение возраста, скажем, от базовой даты и смещения в любых единицах (дни, недели и т. Д.). Я хочу сказать, что часть кода не должна иметь каких-либо странных исходящих зависимостей от остальной части системы. Он просто вычисляет возраст из пары входных значений, и в этом случае модульный тест может подтвердить правильное поведение, которое, вероятно, вызовет исключение, если смещение дает отрицательный возраст.
Крейг,
Я не имел в виду какие-либо расчеты. Если модель хранит часть данных, она может подтвердить, что данные принадлежат правильному домену. В этом случае доменом является множество неотрицательных целых чисел. Расчеты должны выполняться в контроллере (в MVC), и в этом примере вычисление возраста будет отдельным тестом.
4

Вот мое мнение: все тесты имеют стоимость:

  • начальное время и усилия:
    • думать о том, что тестировать и как это проверить
    • выполнить тест и убедиться, что он тестирует то, что он должен
  • текущее обслуживание
    • убедиться, что тест все еще делает то, что должен делать, так как код развивается естественным образом
  • запустить тест
    • время исполнения
    • анализируя результаты

Мы также стремимся к тому, чтобы все тесты приносили пользу (и, по моему опыту, почти все тесты дают преимущества):

  • Спецификация
  • выделить угловые случаи
  • предотвратить регресс
  • автоматическая проверка
  • примеры использования API
  • количественная оценка конкретных свойств (время, пространство)

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

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

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

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


источник
2

Что это блок испытания, на самом деле? И действительно ли здесь такая большая дихотомия?

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

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

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

Я собираюсь сказать нет . Это интеграция тест. И те, кто определенно имеют свое место, но это также другая тема.

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

Поэтому юнит-тест должен иметь очень ограниченную область применения.

Что означает , что код протестирован с помощью модульного тестирования должен иметь очень ограниченную сферу применения.

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

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

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

Я думаю, что понятие «модульного теста», тестирующего нетривиальную, сложную и сложную логику, немного оксюморон.

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

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

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

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

Но сама логика отдельных юнит-тестов остается максимально простой.

Craig
источник
1

Это, конечно, мое мнение, но, потратив последние несколько месяцев на изучение функционального программирования на fsharp (из опыта C #), я понял несколько вещей.

Как указывалось в ОП, обычно мы видим два типа «модульных тестов». Тесты, которые охватывают входы и выходы метода, которые, как правило, являются наиболее ценными, но их трудно выполнить для 80% системы, в которой меньше говорится об «алгоритмах» и больше о «абстракциях».

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

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

С тех пор, как я начал писать код на Fsharp, мне ни для чего не понадобился фреймворк, и я даже полностью избавился от зависимости от контейнера IOC. Мои тесты основаны на бизнес-потребностях и ценности, а не на сложных уровнях абстракции, обычно необходимых для достижения композиции в императивном программировании.

Митчелл Ли
источник