Как избежать дублирования логики с помощью Mocks - PullRequest
7 голосов
/ 13 марта 2009

У меня следующий вызов, и я не нашел хорошего ответа. Я использую среду Mocking (в данном случае JMock), чтобы позволить модульным тестам быть изолированными от кода базы данных. Я издеваюсь над доступом к классам, которые включают логику базы данных, и отдельно тестирую классы базы данных, используя DBUnit.

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

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

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

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

Есть предложения?

UPDATE:

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

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

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

[Изменить после второго редактирования Аарона Дигуллы на его ответ]:

Правильно, это именно то, что я делаю (за исключением того, что есть некоторое дальнейшее взаимодействие с БД в классе, который тестируется через DBUnit и взаимодействует с базой данных во время ее тестов, но это та же идея) , Итак, скажем, нам нужно изменить поведение базы данных, чтобы результаты были другими. Тест, использующий макет, будет продолжать проходить, если 1) кто-то не помнит или 2) он не сломается при интеграции Таким образом, возвращаемые значения (скажем) хранимой процедуры по существу дублируются в тестовых данных макета. Теперь, что беспокоит меня о дублировании, это то, что логика дублируется, и это является тонким нарушением СУХОГО. Возможно, так оно и есть (в конце концов, есть причина для интеграционных тестов), но я чувствовал, что вместо этого я что-то упускаю.

[Изменить при запуске щедрости]

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

[Изменить присуждение награды]

Спасибо всем, кто потратил время, отвечая на вопрос. Победитель научил меня чему-то новому о том, как думать о передаче данных между двумя слоями, и первым получил ответ.

Ответы [ 11 ]

4 голосов
/ 09 мая 2009

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

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

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

Итак, подведем итог:

  1. Напишите некоторое интеграционное / функциональное тестирование для выявления проблем с взаимодействующими объектами
  2. Не всегда нужно издеваться над соавторами - используйте свое лучшее мнение
4 голосов
/ 08 мая 2009

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

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

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

Кодер, который добавляет какой-то новый результат возврата в систему, отвечает за добавление модульного теста для обработки этого случая. Если этот код также на 100% уверен, что никак не может , что нулевой результат может быть возвращен сейчас, тогда он также может удалить старый модульный тест. Но почему ты? Модульный тест правильно описывает поведение тестируемого объекта, когда он получает нулевой результат. Что произойдет, если вы измените серверную часть вашей системы на какую-то новую базу данных, которая вернет ноль? Что если спецификация вернется к нулю? С тем же успехом вы можете сохранить тест, поскольку в том, что касается вашего объекта, он действительно может получить что-либо от внешнего ресурса, и он должен корректно обрабатывать все возможные случаи.

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

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

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

Абстракция вашей базы данных использует ноль, чтобы означать "результаты не найдены". Игнорируя тот факт, что передача нулевого значения между объектами плохая идея, ваши тесты не должны использовать этот нулевой литерал, когда они хотят проверить, что происходит, когда ничего не найдено. Вместо этого используйте константный или построитель тестовых данных , чтобы ваши тесты ссылались только на то, какая информация передается между объектами, а не на то, как эта информация представляется. Затем, если вам нужно изменить способ представления слоя базы данных «результаты не найдены» (или какую-либо информацию, на которую опирается ваш тест), у вас есть только одно место в ваших тестах, чтобы изменить это.

2 голосов
/ 13 марта 2009

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

[РЕДАКТИРОВАТЬ] На основе комментария: Макет не должен ничего делать, кроме как разрешать создавать экземпляр тестируемого класса и собирать дополнительную информацию. В частности, он никогда не должен влиять на результат того, что вы хотите проверить.

[EDIT2] Проверка базы данных означает, что вам все равно, работает ли драйвер БД. Вы хотите знать, может ли ваш код правильно интерпретировать данные, возвращаемые БД. Кроме того, это единственный способ проверить, правильно ли работает ваша обработка ошибок, потому что вы не можете сказать реальному драйверу БД: «когда вы видите этот SQL, выведите эту ошибку». Это возможно только с макетом.

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

  • У меня есть тесты, которые проверяют, работает ли SQL. Каждый SQL выполняется один раз для статической тестовой БД, и я проверяю, что возвращенные данные - это то, что я ожидаю.
  • Все остальные тесты выполняются с пробным соединителем БД, который возвращает предопределенные результаты. Мне нравится получать эти результаты, выполняя код для базы данных, регистрируя первичные ключи где-то. Затем я пишу инструмент, который берет эти первичные ключи и сбрасывает код Java с макетом в System.out. Таким образом, я могу очень быстро создавать новые тесты, и они будут отражать «правду».

    Еще лучше, я могу воссоздать старые тесты (при изменении БД), снова запустив старые идентификаторы и мой инструмент

1 голос
/ 13 мая 2009

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

Модульные тесты не должны заботиться о деталях реализации.

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

Имеет ли смысл из внешнего POV, что эта вещь возвращает NULL, или это удобное следствие, потому что в клиенте могут быть сделаны прямые предположения относительно значения этого NULL? NULL должен означать void / nix / nada / недоступный без какого-либо другого значения.

Если вы планируете гранулировать это условие позже, то вам следует обернуть проверку NULL во что-то, что возвращает либо информативное исключение, enum или явное имя bool.

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

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

1 голос
/ 13 мая 2009

Вот как я понимаю ваш вопрос:

Вы используете фиктивные объекты ваших сущностей для тестирования бизнес-уровня вашего приложения с использованием JMock. Вы также тестируете свой уровень DAO (интерфейс между вашим приложением и базой данных), используя DBUnit, и передаете реальные копии ваших объектов-сущностей, заполненных известным набором значений. Поскольку вы используете 2 различных метода для подготовки ваших тестовых объектов, ваш код нарушает DRY, и вы рискуете, что ваши тесты будут синхронизированы с реальностью при изменении кода.

Фолвер говорит ...

Это не совсем то же самое, но это, безусловно, напоминает мне статью Мартина Фаулера Насмешки не окурки . Я рассматриваю маршрут JMock как способ mockist , а маршрут «реальных объектов» как способ classicist для проведения тестирования.

Один из способов сделать СУХОЙ, насколько это возможно, при решении этой проблемы - быть более классицистом , чем издевателем Может быть, вы можете скомпрометировать и использовать реальные тесты ваших объектов bean в своих тестах.

Создатели пользователей, чтобы избежать дублирования

В одном проекте мы создали Makers для каждого из наших бизнес-объектов. Создатель содержит статические методы, которые будут создавать копию данного объекта сущности, заполненную известными значениями. Затем, какой бы тип объекта вам ни понадобился, вы можете вызвать создателя этого объекта и получить его копию с известными значениями для использования в вашем тестировании. Если у этого объекта есть дочерние объекты, ваш производитель будет вызывать их для создания сверху вниз, и вы получите столько граф объектов, сколько вам нужно. Вы можете использовать эти maker-объекты для всех ваших тестов - передавать их в БД при тестировании уровня DAO, а также передавать их вызовам сервисов при тестировании бизнес-сервисов. Поскольку производители могут быть использованы повторно, это довольно СУХОЙ подход.

Однако, для JMock вам все еще нужно будет использовать макет уровня DAO при тестировании уровня обслуживания. Если ваша служба звонит в DAO, вы должны убедиться, что вместо этого ей вводят ложные сообщения. Но вы все равно можете использовать свои Makers точно так же - когда настраиваете свои ожидания, просто убедитесь, что ваш поддельный DAO передает ожидаемый результат, используя Maker для соответствующего объекта сущности. Таким образом, мы все еще не нарушаем СУХОЙ.

Хорошо написанные тесты сообщат вам, когда код изменится

Мой последний совет, чтобы избежать проблем с изменением кода с течением времени: всегда иметь тест, который обращается к нулевым входам. Предположим, что когда вы впервые создаете свой метод, значения null недопустимы. У вас должен быть тест, который проверяет, что выбрасывается исключение, если используется значение null. Если позднее значения NULL станут приемлемыми, код вашего приложения может измениться, чтобы значения NULL обрабатывались по-новому, и исключение больше не создавалось. Когда это произойдет, ваш тест начнет проваливаться, и у вас будет «хедс-ап», ​​что вещи не синхронизированы.

1 голос
/ 09 мая 2009

Я бы хотел сузить проблему до ее сути.

Проблема

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

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

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

Вы запускаете свои тесты - все проходят .

Но со временем вы что-то забыли ... издевательство никогда не возвращает null. Таким образом, n тестов для n классов, которые используют макет, не тестируют для null.

Ваш QA не пройдёт - хотя ваши тесты не прошли.

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

Решение

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

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

Как уже упоминали другие пользователи - покрытие кода выявляет некоторые непроверенные случаи. Но пропущенный код обработки ошибок и пропущенный тест соответствия не будут отображаться в покрытии кода. (Покрытие кода 100% не означает, что вы что-то не упустили.)

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

Если вы решите null как допустимое значение - этот тест позже не пройдёт (из-за пропущенных исключений). Таким образом, вы получите список сбоев.

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


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

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

Если я правильно понимаю вопрос, у вас есть бизнес-объект, который использует модель. Существует тест на взаимодействие между BO и моделью (тест A), и есть другой тест, который тестирует взаимодействие между моделью и базой данных (тест B). Тест B изменяется, возвращая объект, но это изменение не влияет на тест A, потому что модель теста A является ложной.

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

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

0 голосов
/ 09 мая 2009

Я думаю, что ваша проблема нарушает принцип замещения Лискова:

Подтипы должны заменять свои базовые типы

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

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

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

Итак,

class NeedsWork(IWorker b) { DoSth() { b.Work() }; }
...
AppBuilder() { INeedWork GetA() { return new NeedsWork(new Worker()); } }
  1. Измените IWorker так, чтобы он отражал новые потребности NeedsWork.
  2. Измените DoSth, чтобы он работал с новой абстракцией, которая удовлетворяет его новым потребностям.
  3. Протестируйте NeedsWork и убедитесь, что он работает с новым поведением.
  4. Измените все реализации (рабочий в этом сценарии), которые вы предоставляете для IWorker (что вы сейчас пытаетесь сделать в первую очередь).
  5. Test Worker, чтобы соответствовать новым ожиданиям.

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

0 голосов
/ 08 мая 2009

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

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

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

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

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

...