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

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

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

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

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

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

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

  • «… модульное тестирование …»
  • «… тестовый class для каждого classа в системе …»
  • «… код тестового кода / производственного кода … идеально подходит для приближения к соотношению 1: 1 …»

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

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

В итоге:

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Первичный приоритет – проверять публичные интерфейсы, затем защищать, а затем частные.

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

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

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

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

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

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

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

    привет

    Я лично испытываю защищенные части, потому что они являются «общедоступными» для унаследованных типов …

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

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

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

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

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

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

    • Поэтому: если программист пишет и передает одну «единицу», то они также должны были протестировать эту единицу, вполне возможно, написав «единичный тест»,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Interesting Posts
    Давайте будем гением компьютера.