Разработка модульных тестов для системы с состоянием

20

Фон

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

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

Источник: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

Они также говорят такие вещи (в дальнейшем именуемые «принцип частного метода» или PMP ):

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

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

Источник: Как вы тестируете приватные методы?

ситуация

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

  • SAP предлагает, чтобы я не проверял «процедуру создания состояния», я должен предположить, что состояние - это то, что я ожидаю от кода сборки, а затем проверить одно изменение состояния, которое я пытаюсь проверить

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

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

durron597
источник
2
Я не думаю, что вы найдете элегантное решение для этого. Общий подход заключается не в том, чтобы начинать систему с состояния с самого начала, что не поможет вам при тестировании уже созданного. Преобразование его в состояние без гражданства, вероятно, также не стоит затрат.
Довал
@Doval: Пожалуйста, объясните, как сделать что-то вроде телефона (SIP UserAgent) не государственным. Ожидаемое поведение этого устройства указывается в RFC с использованием диаграммы перехода состояний.
Барт ван Инген Шенау
Вы копируете / вставляете / редактируете свои тесты или пишете служебные методы для обмена общими настройками / разборками / функциями? Хотя некоторые тестовые сценарии, безусловно, могут стать длинными и раздутыми, это не должно быть таким уж распространенным явлением. В системе с состоянием я бы ожидал общую процедуру установки, где конечное состояние является параметром, и эта процедура возвращает вас в состояние, которое вы хотите проверить. Кроме того, в конце каждого теста у меня будет метод разрыва, который возвращает вас к известному начальному состоянию (если это необходимо), поэтому ваш метод настройки будет работать правильно, когда начнется следующий тест.
Данк
По касательной, но я также добавлю, что диаграммы состояний являются средством коммуникации, а не указом о реализации, даже если они есть в RFC. Пока вы соответствуете описанной функциональности, вы соответствуете стандарту. У меня была пара случаев, когда я преобразовывал действительно сложные реализации перехода состояний (как определено в RFC) в действительно простую общую функциональность обработки. В одном случае я помню, как избавился от пары тысяч строк кода, когда понял, что, кроме пары флагов, около 5 состояний делают то же самое, когда вы переименовываете «скрытые» общие элементы.
Данк

Ответы:

15

Перспектива:

Итак, давайте сделаем шаг назад и спросим, ​​с чем TDD пытается нам помочь. TDD пытается помочь нам определить, верен ли наш код или нет. И по правде говоря, я имею в виду "соответствует ли код бизнес-требованиям?" Суть в том, что мы знаем, что в будущем потребуются изменения, и мы хотим убедиться, что наш код останется верным после того, как мы внесем эти изменения.

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

Принципы - SAP:

Хотя я не эксперт по TDD, я думаю, что вы упускаете часть того, чему пытается научить принцип единого утверждения (SAP). SAP можно переформулировать как «проверять по одной вещи за раз». Но TOTAT не так легко скатывается с языка, как SAP.

Тестирование по одной вещи за раз означает, что вы сосредоточены на одном случае; один путь; одно граничное условие; один случай ошибки; один независимо один тест. И основная идея заключается в том, что вам нужно знать, что сломалось при неудачном тестировании, чтобы вы могли решить проблему быстрее. Если вы тестируете несколько условий (т. Е. Более чем одно) в одном тесте, и тест не пройден, у вас намного больше работы. Сначала нужно определить , какие из многочисленных случаев не удалось , а затем выяснить , почему , что дело не удалось.

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

Принципы - PMP:

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

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


Прикладной TDD ( для вас )

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

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

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

Далее вам нужно построить тесты для проверки обработки данных. Некоторые из этих тестов состояния будут повторно использованы при создании тестов обработки данных. Например, предположим, у вас есть метод, Foo()который имеет разные выходные данные, основанные на состояниях Initи State1. Вы захотите использовать свой ChangeFooToState1тест в качестве шага настройки, чтобы проверить вывод, когда « Foo()в State1».

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

Прежде всего, вы должны признать, что вы используете что-то в качестве теста в одной ситуации и настройку в другой ситуации. С одной стороны, это кажется прямым нарушением SAP. Но если вы логически объявляете, ChangeFooToState1что преследуете две цели, вы все равно отвечаете духу того, чему нас учит SAP. Когда вам нужно убедиться, что Foo()изменения состояний, то вы используете ChangeFooToState1в качестве теста. А когда нужно проверить « Foo()вывод, когда в State1», то вы используете ChangeFooToState1в качестве установки.

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

Собираем это вместе:

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

Если вы будете следовать этому подходу, bloated, complicated, long, and difficult to writeтесты станут немного проще в управлении. В общем, они должны быть меньше, и они должны быть более краткими (то есть менее сложными). Вы должны заметить, что тесты также более разрозненные или модульные.

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


источник
11

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

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

Это часто является признаком того, что вы вкладываете слишком много в один класс. Если у вас есть требования к состоянию, вам нужен класс, который управляет государством, и ничего больше. Классы, которые поддерживают это, должны быть без гражданства. Для вашего примера SIP синтаксический анализ пакета должен быть полностью без сохранения состояния. У вас может быть класс, который анализирует пакет, а затем вызывает что-то вроде sipStateController.receiveInvite()управления переходами состояний, который сам вызывает другие классы без состояния для выполнения таких действий, как звонок на телефон.

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

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

Карл Билефельдт
источник
Просто для протокола, пример SIP был моим, а не из OP. А некоторым конечным автоматам может потребоваться несколько вызовов методов, чтобы привести их в нужное состояние для определенного теста.
Барт ван Инген Шенау
+1 за «вы не можете полностью избежать состояния, но вы можете минимизировать и изолировать его». Я не мог согласиться Государство - необходимое зло в программном обеспечении.
Брэндон
0

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

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

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

Сору
источник