Приводит ли модульное тестирование к преждевременному обобщению (особенно в контексте C ++)?


20

Предварительные заметки

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

Я возьму то, что там, и это говорит: модульное тестирование в смысле «тестирования самого маленького изолируемого модуля приложения», из которого фактически возникает этот вопрос

Проблема изоляции

Какой самый маленький изолируемый модуль программы. Ну, как я понимаю, это (очень?) Зависит от того, на каком языке вы кодируете.

Micheal Feathers рассказывает о концепции шва : [WEwLC, p31]

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

И не вдаваясь в детали, я понимаю шов - в контексте модульного тестирования - место в программе, где ваш «тест» может взаимодействовать с вашим «модулем».

Примеры

Модульное тестирование - особенно в C ++ - требует от тестируемого кода добавить больше швов , которые будут строго требоваться для данной проблемы.

Пример:

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

Вопрос

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

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

Я помню эмпирическое правило, которое гласило: не обобщайте, пока вам не понадобится / пока не появится второе место, которое использует код. С юнит-тестами всегда есть второе место, где используется код, а именно юнит-тест. Так достаточно ли этой причины для обобщения?


8
Распространенным мемом является то, что любой шаблон может быть чрезмерно использован для превращения в анти-шаблон. То же самое относится и к TDD. Можно добавить тестируемые интерфейсы после точки уменьшения отдачи, когда проверяемый код меньше добавленных обобщенных тестовых интерфейсов, а также в область слишком низких затрат и выгод. Казуальная игра с добавленными интерфейсами для тестирования, подобная ОС для дальнего космоса, может полностью упустить свое рыночное окно. Убедитесь, что добавленное тестирование находится перед этими точками перегиба.
hotpaw2

@ hotpaw2 Богохульство! :)
maple_shaft

Ответы:


23

Модульное тестирование - особенно в C ++ - требует от тестируемого кода добавить больше швов, которые будут строго требоваться для данной проблемы.

Только если вы не считаете тестирование неотъемлемой частью решения проблем. Для любой нетривиальной проблемы это должно быть не только в мире программного обеспечения.

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

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

Является ли способ, по которому модульные тесты требуют структурирования кода приложения, «только» выгодным для модульных тестов, или это действительно выгодно для структуры приложений?

По моему опыту, тестируемый дизайн полезен в целом, а не только для модульных тестов. Эти преимущества приходят на разных уровнях:

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

Я помню эмпирическое правило, которое гласило: не обобщайте, пока вам не понадобится / пока не появится второе место, которое использует код. С юнит-тестами всегда есть второе место, где используется код, а именно юнит-тест. Так достаточно ли этой причины для обобщения?

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

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


Сэр, я приветствую вас.
GordonM

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

4

Я собираюсь бросить в вас Путь Фестиваля , но подведу итог:

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

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

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

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

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


Я собирался добавить то же самое: сделать API, проверить API, снаружи.
Кристофер Махан

2

TDD и Unit Testing, это хорошо для программы в целом, а не только для модульных тестов. Причина этого в том, что это полезно для мозга.

Это презентация о конкретной среде ActionScript с именем RobotLegs. Тем не менее, если вы пролистаете первые 10 слайдов или около того, он начинает добираться до хороших частей мозга.

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


1

тестирование наименьшего изолируемого блока приложения

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

Например, я однажды отлаживал сетевой класс, который имел (среди прочих битов) 2 метода: 1 для установки IP-адреса, другой для установки номера порта. Естественно, это были очень простые методы, которые легко могли бы пройти самый простой тест, но если вы зададите номер порта, а затем установите IP-адрес, он не будет работать - установщик ip перезаписывает номер порта по умолчанию. Таким образом, вы должны были протестировать класс в целом, чтобы убедиться в правильном поведении, но я думаю, что концепция TDD отсутствует, но BDD дает вам. Вам не нужно тестировать каждый крошечный метод, когда вы можете протестировать самую разумную и наименьшую область всего приложения - в данном случае сетевой класс.

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

Таким образом, учитывая это, вам не нужно структурировать ваш код определенным образом для достижения TDD, но уровень тестирования, которого вы достигнете, будет зависеть от структуры вашего кода - если у вас есть монолитный GUI, вся логика которого тесно связана с структура GUI, тогда вам будет сложнее изолировать эти части, но вы все равно можете написать модульный тест, где «unit» ссылается на GUI, а вся работа с внутренними БД подвергается проверке. Это крайний пример, но он показывает, что вы все равно можете проводить на нем автоматическое тестирование.

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


0

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.