Следует ли тестировать внутреннюю реализацию или только тестировать публичное поведение? - PullRequest
43 голосов
/ 13 мая 2009

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

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

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

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

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

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

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

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

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

В итоге:

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

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

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

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

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


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

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

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

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

Ответы [ 15 ]

31 голосов
/ 13 мая 2009

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

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

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

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

17 голосов
/ 13 мая 2009

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

9 голосов
/ 13 мая 2009

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

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

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

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

8 голосов
/ 13 мая 2009

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

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

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

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

2 голосов
/ 29 августа 2009

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

2 голосов
/ 13 мая 2009

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

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

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

1 голос
/ 27 июля 2010

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

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

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

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

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

С уважением ameet

1 голос
/ 09 октября 2009

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

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

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

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

1 голос
/ 15 августа 2009

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

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

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

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

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

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

0 голосов
/ 09 октября 2009

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

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

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

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

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

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

...