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

Следует ли тестировать внутреннюю реализацию или тестировать общественное поведение?

Данное программное обеспечение, где...

  • Система состоит из нескольких подсистем
  • Каждая подсистема состоит из нескольких компонентов
  • Каждый компонент реализуется с использованием многих классов

... Мне нравится писать автоматические тесты каждой подсистемы или компонента.

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

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

Я думаю, что эта политика контрастирует с документом, например Рефакторинг тестового кода, в котором говорится, что...

  • "... unit testing..."
  • "... тестовый класс для каждого класса в системе..."
  • "... код тестового кода/производственного кода... идеально подходит для приближения к соотношению 1:1..."

... все из которых, я полагаю, не согласен (или, по крайней мере, не практикую).

Мой вопрос: если вы не согласны с моей политикой, объясните, почему? В каких сценариях недостаточно эта степень тестирования?

Вкратце:

  • Общедоступные интерфейсы тестируются (и повторно тестируются) и редко меняются (они добавляются, но редко изменяются)
  • Внутренние API-интерфейсы скрыты за общедоступными API-интерфейсами и могут быть изменены без перезаписи тестовых примеров, которые проверяют публичные API-интерфейсы

Сноска: некоторые из моих "тестовых случаев" фактически реализованы как данные. Например, тестовые примеры для пользовательского интерфейса состоят из файлов данных, которые содержат различные пользовательские входы и соответствующие ожидаемые выходы системы. Тестирование системы означает наличие тестового кода, который считывает каждый файл данных, повторяет ввод в систему и утверждает, что он получает соответствующий ожидаемый результат.

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

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

Сноска: по "компоненту" я имею в виду что-то вроде "одной DLL" или "одной сборки"... что-то достаточно большое, чтобы быть видимым на архитектуре или диаграмме развертывания системы, часто реализуемой с использованием десятков или 100 классы и публичный API, который состоит только из 1 или нескольких интерфейсов... что-то, что может быть назначено одной команде разработчиков (где другой компонент назначен другой команде), и, следовательно, согласно Закон Конвей, имеющий относительно стабильный публичный API.


Сноска: статья объектно-ориентированное тестирование: миф и реальность гласит:

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

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

Я должен добавить, что я выполняю функциональное тестирование whitebox: я вижу код (в реализации), и я пишу функциональные тесты (которые приводят публичный API) для реализации различных ветвей кода (подробности реализации функции).

4b9b3361

Ответ 1

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

Ответ 2

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

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

Unit-test здесь, чтобы проверить, что 10 строк кода, который вы только что написали, делают то, что он должен делать. Это дает вам больше уверенности в вашем коде.

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

Ответ 3

У меня нет моей копии Lakos передо мной, поэтому вместо цитирования я просто укажу, что он делает лучше, чем я объясню, почему тестирование важно на всех уровнях.

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

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

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

Ответ 4

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

  • Внутренний Unit Test:
    Ожидается, что разработчики создадут модульные тесты для всего кода, который они пишут (читайте: каждый метод). Модульные тесты должны охватывать положительные условия тестирования (работает ли мой метод?) И отрицательные условия теста (метод бросает ArgumentNullException, когда один из моих необходимых аргументов равен NULL?). Мы обычно включаем эти тесты в процесс сборки с помощью инструмента, такого как CruiseControl.net
  • Тест системного теста/сборки:
    Иногда этот шаг называется чем-то другим, но это когда мы начинаем тестировать публичную функциональность. Как только вы узнаете, что все ваши отдельные подразделения функционируют должным образом, вы хотите знать, что ваши внешние функции также работают так, как вы думаете. Это форма функциональной проверки, поскольку цель состоит в том, чтобы определить, работает ли вся система так, как она должна. Обратите внимание, что это не включает никаких точек интеграции. Для системного теста вы должны использовать вместо себя оригинальные интерфейсы, чтобы вы могли управлять выводами и создавать тестовые примеры вокруг него.
  • Тест системной интеграции:
    На этом этапе процесса вы хотите подключить свои точки интеграции к системе. Например, если вы используете систему обработки кредитных карт, на этом этапе вы захотите включить живую систему, чтобы убедиться, что она все еще работает. Вы хотели бы выполнить аналогичное тестирование для тестирования системы/сборки.
  • Проверка функциональной проверки:
    Функциональная проверка - это пользователи, которые запускают систему или используют API для проверки того, что она работает должным образом. Если вы создали систему выставления счетов, это этап, на котором вы будете выполнять свои тестовые сценарии из конца в конец, чтобы убедиться, что все работает по мере его разработки. Это, очевидно, критический этап в этом процессе, поскольку он говорит вам, выполнили ли вы свою работу.
  • Сертификационный тест:
    Здесь вы ставите реальных пользователей перед системой и позволяете им идти на нее. В идеале вы уже тестировали свой пользовательский интерфейс в какой-то момент со своими заинтересованными сторонами, но на этом этапе вам расскажет, нравится ли вашей целевой аудитории ваш продукт. Возможно, вы слышали, что это называлось "релиз-кандидатом" других поставщиков. Если на этом этапе все будет хорошо, вы знаете, что хорошо двигаться в производство. Сертификационные тесты всегда должны выполняться в той же среде, которую вы будете использовать для производства (или, по крайней мере, в одинаковой среде).

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

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

Ответ 5

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

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

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

Ответ 6

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

Ответ 7

Вы не должны слепо думать, что единица == класс. Я думаю, что это может быть контрпродуктивным. Когда я говорю, что пишу unit test, я тестирую логическую единицу - "что-то", которая обеспечивает некоторое поведение. Единица может быть одним классом, или это может быть несколько классов, работающих вместе, чтобы обеспечить такое поведение. Иногда он начинается как один класс, но развивается, чтобы стать тремя или четырьмя классами позже.

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

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

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

Разделение цели тестирования/проверки вашего программного обеспечения и использование тестов/примеров для управления вашим дизайном/реализацией может значительно разъяснить эту дискуссию.

Обновление: Кроме того, есть два способа сделать TDD: внешний и внутренний. BDD продвигает наружу, что приводит к более высоким уровням тестов/спецификаций. Однако, если вы начнете с деталей, вы будете писать подробные тесты для всех классов.

Ответ 8

Я согласен с большинством сообщений здесь, однако я бы добавил следующее:

Существует первичный приоритет для проверки открытых интерфейсов, затем защиты, а затем частного.

Обычно общедоступные и защищенные интерфейсы представляют собой сводку комбинации частных и защищенных интерфейсов.

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

Ответ 9

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

Ответ 10

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

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

Ответ 11

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

Ответ 12

[Ответ на мой вопрос]

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

  • Аксиома: каждый программист должен проверить свой собственный код

  • Поэтому: если программист пишет и доставляет одну "единицу", то они также должны были протестировать эту единицу, вполне возможно, написав "unit test"

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

Аналогично, практика создания "макетных" компонентов, которые вы можете протестировать против:

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

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

Ответ 13

Аксиома: каждый программист должен проверить свой собственный код

Я не думаю, что это универсально.

В криптографии есть известная поговорка: "Легко создать такой шифр, чтобы вы не знали, как его сломать самостоятельно".

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

Ваше доверие сделает вас менее бдительным тестером. Тот, кто не делится вашим опытом с кодом, не будет иметь проблемы.

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

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

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

Ответ 14

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

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

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

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

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

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

Ответ 15

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

Но вы должны иметь возможность развивать кукольный сервис или компонент, используя подход Test First. т.е. определить открытый интерфейс и проверить его на базовые функции. он потерпит неудачу. Реализуйте эту базовую функциональность, используя API-интерфейсы классов фона. Write API, чтобы удовлетворить только этот тест. Затем продолжайте спрашивать, что услуга может сделать больше и развиваться.

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

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

Сообщите мне, если вы разработали хороший процесс, который работает для вас... с вашего первого сообщения.

С уважением ameet