Как избежать дублирования логики с Mocks
У меня есть следующий вызов, и я не нашел хорошего ответа. Я использую Mocking framework (JMock в этом случае), чтобы позволить модульным тестам быть изолированными от кода базы данных. Я издеваюсь над доступом к классам, которые включают логику базы данных, и отдельно тестирую классы базы данных с помощью DBUnit.
проблема в том, что я замечаю шаблон, где логика концептуально дублируется в нескольких местах. Например мне нужно определить, что значение в база данных не существует, поэтому я могу вернуть null из метода в этом случае. Так у меня есть класс доступа к базе данных, который выполняет взаимодействие с базой данных, и возвращает соответствующее значение null. Затем у меня есть класс бизнес-логики, который получает null от макета, а затем тестируется, чтобы действовать соответствующим образом, если значение равно null.
теперь что делать, если в будущем это поведение должно измениться и возврат null больше не подходит, скажем, потому что состояние стало более сложным, поэтому мне нужно возвращает объект, который представляет значение не существует и некоторые дополнительные факты из базы данных.
теперь, если я изменю поведение класса базы данных, чтобы больше не возвращать null в этом случае, класс бизнес-логики все равно будет функционировать, и ошибка будет поймана только в QA, если кто-то не вспомнит соединение или должным образом не будет следовать обычаям метода.
Я упал, как будто мне чего-то не хватает, и должен быть лучший способ избежать этого концептуальное дублирование или, по крайней мере, его проверка, чтобы в случае изменения тот факт, что изменение не распространяется, не прошел модульный тест.
какие предложения?
обновление:
позвольте мне попытаться прояснить мой вопрос. Я думаю о том, когда код развивается с течением времени, как гарантировать, что интеграция не разрывается между классами, протестированными через макет и фактическую реализацию classed, которую представляет макет.
например, у меня только что был случай, когда у меня был метод, который был первоначально создан и не ожидал значений null, поэтому это не был тест на реальном объекте. Затем пользователь класса (проверено через макет) был расширен, чтобы передать null в качестве параметра при определенных обстоятельствах. При интеграции это сломалось, потому что реальный класс не был протестирован на null. Теперь при построении этих классов сначала это не имеет большого значения, потому что вы тестируете оба конца, когда вы строите, но если дизайн должен развиваться через два месяца, когда вы как правило, забывают о деталях, как бы вы проверили взаимодействие между этими двумя наборами объектов (тот, который протестирован через макет против фактической реализации)?
основная проблема, по-видимому, заключается в дублировании (что нарушает принцип DRY), ожидания действительно сохраняются в двух местах, хотя отношения концептуальны, нет фактического дубликата кода.
[редактирование после второго редактирования Аарон Digulla на его ответ]:
право, что это именно то, что я делаю (за исключением того, что есть некоторое дальнейшее взаимодействие с БД в классе, который тестируется через DBUnit и взаимодействует с базой данных во время ее тестов, но это та же идея). Итак, теперь скажем, что нам нужно изменить поведение базы данных, чтобы результаты были разными. Тест с использованием макета будет продолжать проходить, если 1) кто-то не помнит или 2) он ломается в интеграции. Таким образом, возвращаемые значения хранимой процедуры (скажем) базы данных по существу дублируются в тестовые данные макета. Теперь, что меня беспокоит в дублировании, так это то, что логика дублируется, и это тонкое нарушение DRY. Возможно, так оно и есть (в конце концов, есть причина для интеграционных тестов), но я чувствовал, что вместо этого я что-то упускаю.
[правка при запуске bounty]
чтение взаимодействия с Аароном доходит до сути вопроса, но то, что я действительно ищу, - это некоторое представление о том, как избежать или управлять явное дублирование, так что изменение поведения реального класса будет отображаться в модульных тестах, которые взаимодействуют с макетом как что-то сломанное. Очевидно, что это не происходит автоматически, но может быть способ правильно спроектировать сценарий.
[правка при присуждении награды]
спасибо всем, кто потратил время, отвечая на вопрос. Победитель учил меня чему-то новому о том, как думать о передаче данных между двумя слоями, и надо сначала ответ.
11 ответов
ваша абстракция базы данных использует null, чтобы означать "нет результатов". Игнорируя тот факт, что передавать null между объектами-плохая идея, ваши тесты не должны использовать этот нулевой литерал, когда они хотят проверить, что происходит, когда ничего не найдено. Вместо этого используйте константу или тестовых данных построитель чтобы ваши тесты ссылались только на то, какая информация передается между объектами, а не на то, как эта информация представлена. Затем, если вам нужно изменить способ, которым слой базы данных представляет собой "не найдено результатов" (или любую информацию, на которую опирается ваш тест), у вас есть только одно место в тестах, чтобы изменить это.
вы принципиально просите невозможного. Вы просите модульные тесты предсказать и уведомить вас об изменении поведения внешнего ресурса. Без написания теста для создания нового поведения, как они могут знать?
то, что вы описываете, добавляет новое состояние, которое должно быть проверено на - вместо нулевого результата, теперь есть какой-то объект, выходящий из базы данных. Как ваш тестовый набор может знать, какое предполагаемое поведение объект под тестом должен быть для какого-то нового, случайного объекта? Тебе нужно написать новый тест.
макет не "плохо себя ведет", как вы прокомментировали. Макет делает именно то, для чего вы его настроили. Тот факт, что спецификация изменилась, не имеет никакого значения для макета. Единственная проблема в этом сценарии заключается в том, что человек, реализовавший изменение, забыл обновить модульные тесты. Я на самом деле не слишком уверен, почему вы думаете, что происходит дублирование проблем на.
кодер, добавляющий новый результат возврата в систему, отвечает за добавление модульного теста для обработки этого случая. Если этот код также на 100% уверен, что там не так что нулевой результат может быть возвращен сейчас, тогда он также может удалить старый модульный тест. Но зачем тебе это? Модульный тест правильно описывает поведение тестируемого объекта при получении нулевого результата. Что произойдет, если вы измените бэкэнд-системы в некоторых новая база данных, которая возвращает null? Что делать, если спецификация вернулась к возвращению null? Вы также можете сохранить тест, поскольку, Что касается вашего объекта, он действительно может получить что-либо от внешнего ресурса, и он должен изящно обрабатывать каждый возможный случай.
вся цель издевательства-отделить ваши тесты от реальных ресурсов. Это не будет автоматически спасать вас от введения ошибок в систему. Если ваш модульный тест точно описывает поведение, когда он получает null, отлично! Но этот тест не должен иметь каких-либо знаний о каком-либо другом государстве и, конечно же, не должен каким-либо образом информироваться о том, что внешний ресурс больше не будет отправлять нули.
Если вы делаете правильный, слабо связанный дизайн, ваша система может иметь любой бэкэнд, который вы можете себе представить. Вы не должны писать тесты с учетом одного внешнего ресурса. Похоже, вы могли бы быть счастливее, если бы добавили некоторые интеграционные тесты, которые используйте свою реальную базу данных, тем самым устраняя насмешливый слой. Это всегда отличная идея для использования с выполнением тестов сборки или здравомыслия / дыма, но обычно является обструктивным для повседневной разработки.
ты ничего не упускаешь. Это слабость в модульном тестировании с макетами объектов. Похоже, вы правильно разбиваете свои модульные тесты на блоки разумного размера. Это хорошо; гораздо чаще люди слишком много тестируют в "единичном" тесте.
к сожалению, при тестировании на этом уровне детализации модульные тесты не охватывают взаимодействие между сотрудничающими объектами. Вам нужно иметь некоторые интеграционные тесты или функциональные тесты, чтобы покрыть это. Я не знаю лучшего ответа.
иногда практично использовать реального сотрудника вместо макета в вашем модульном тесте. Например, при модульном тестировании объекта доступа к данным использование объекта реального домена в модульном тесте вместо макета часто достаточно легко настроить и выполнить так же хорошо. Обратное часто не верно - объекты доступа к данным обычно нуждаются в подключении к базе данных, файловом или сетевом подключении и довольно использование реального объекта данных при модульном тестировании объекта домена превратит модульный тест, который занимает микросекунды в тот, который занимает сотни или тысячи миллисекунд.
Итак:
- напишите некоторые интеграции / функциональное тестирование, чтобы поймать проблемы с сотрудничающими объектами
- не всегда нужно издеваться над сотрудниками - используйте свое лучшее суждение
модульные тесты не могут сказать вам, когда метод внезапно имеет меньший набор возможных результатов. Вот для чего предназначено покрытие кода: оно скажет вам, что код больше не выполняется. Это, в свою очередь, приведет к обнаружению мертвого кода на уровне приложения.
[EDIT] на основе комментария: макет не должен делать ничего, кроме создания экземпляра тестируемого класса и сбора дополнительной информации. Особенно, это должно никогда влияет на результат что вы хотите проверить.
[EDIT2] издевательство над базой данных означает, что вам все равно, работает ли драйвер БД. Вы хотите знать, может ли ваш код правильно интерпретировать данные, возвращаемые БД. Кроме того, это единственный способ проверить, работает ли ваша обработка ошибок правильно, потому что вы не можете сказать реальный драйвер БД "когда вы видите этот SQL, бросьте эту ошибку.- Это возможно только в шутку.
Я согласен, требуется некоторое время, чтобы привыкнуть. Вот что я do:
- у меня есть тесты, которые проверяют, работает ли SQL. Каждый SQL выполняется один раз против статической тестовой БД, и я проверяю, что возвращаемые данные-это то, что я ожидаю.
-
все остальные тесты выполняются с макетом DB connector, который возвращает предопределенные результаты. Мне нравится получать эти результаты, выполняя код в базе данных, регистрируя первичные ключи где-то. Затем я пишу инструмент, который берет эти первичные ключи и сбрасывает Java-код с макетом в систему.из. Таким образом, я могу создавать новые тестовые случаи очень быстро, и тестовые случаи будут отражать "правду".
еще лучше, я могу воссоздать старые тесты (при изменении БД), запустив старые идентификаторы и мой инструмент снова
Я хотел бы сузить проблему до ее ядра.
Проблема
конечно, большинство ваших изменений будут пойманы тестом.
Но есть подмножество сценариев, где ваш тест не потерпит неудачу , хотя он должен:
при написании кода Вы используете свои методы несколько раз. Вы получаете отношение 1:n между определением метода и использованием. Каждый класс, который использует этот метод, будет использовать его макет в соответствующем тесте. Таким образом, макет также используется n раз.
результат ваших методов когда-то ожидалось, что никогда не будет null
. После того, как вы измените это, вы, вероятно, не забудете исправить соответствующий тест. Пока все хорошо.
вы запускаете ваши тесты - все пройдет.
но со временем вы что-то забыли ... макет никогда не возвращает null
. Таким образом, N тест для n классов, которые используют макет, не тестируют для null
.
код QA потерпит неудачу - хотя тесты делали не подвести.
очевидно, вам придется изменить свои другие тесты. Но нет никаких сбоев в работе. Поэтому вам нужно решение, которое работает лучше, чем запоминание всех ссылочных тестов.
Решение
чтобы избежать таких проблем, вам придется писать лучшие тесты с самого начала. Если вы пропустите случаи, когда тестируемый класс должен обрабатывать ошибки или null
значения, у вас просто есть неполное тесты. Это как не испытывать все функции вашего класса.
трудно добавить это позже. - Так что начинайте пораньше и проводите тесты.
как упоминалось другими пользователями-покрытие кода показывает некоторые непроверенные случаи. Но отсутствует код обработки ошибок и отсутствующий соответствующий тест не будет отображаться в покрытии кода. (покрытие кода 100% не означает, что вы ничего не упускаете.)
так напишите хороший тест:предположим внешним миром будьте злыми. это включает не только передать плохие параметры (типа null
значения). ваши насмешки тоже часть внешнего мира. передать null
S и исключения-и наблюдайте, как ваш класс обрабатывает их, как ожидалось.
если вы решили null
чтобы быть допустимым значением-этот тест позже завершится неудачей (из-за отсутствующих исключений).
Таким образом, вы получаете список неудач.
потому что каждый вызывающий класс обрабатывает ошибки или null
different-это не дубликат кода, которого можно избежать. Различное лечение требует различных тестов.
подсказка: держите ваш макет простым и чистым. Переместите ожидаемые возвращаемые значения в метод тестирования. (Ваш МОК может просто передать их обратно.) Избегайте тестирования решений в издевательствах.
вот как я понимаю ваш вопрос:
вы используете макетные объекты ваших сущностей для тестирования бизнес-уровня вашего приложения с помощью JMock. Вы также тестируете свой слой DAO (интерфейс между вашим приложением и вашей базой данных) с помощью DBUnit и передаете реальные копии ваших объектов сущности, заполненных известным набором значений. Поскольку вы используете 2 разных метода подготовки тестовых объектов, ваш код нарушает DRY, и вы рискуете своими тестами выход из синхронизации с реальностью при изменении кода.
Фаулером говорит...
Это не совсем то же самое, но это, безусловно, напоминает мне Мартина Фаулера насмешки-это не обрубки статьи. Я вижу маршрут JMock как mockist путь и маршрут "реальных объектов" как классицизм способ выполнения тестирования.
один из способов быть как можно более сухим при решении этой проблемы - быть более классицизм тогда a mockist. Возможно, вы можете скомпрометировать и использовать реальные копии ваших объектов bean в своих тестах.
пользовательские производители, чтобы избежать дублирования
то, что мы сделали в одном проекте, - это создать создателей для каждого из наших бизнес-объектов. Создатель содержит статические методы, которые будут создавать копию данного объекта сущности, заполненную известными значениями. Затем, какой бы объект вам ни понадобился, вы можете назвать создателя этого объекта и получить его копию с известными значениями для тестирования. Если у этого объекта есть дочерние объекты, ваш создатель вызовет создателей для детей, чтобы построить его сверху вниз, и вы получите столько полного графа объектов, сколько вам нужно. Вы можете использовать эти объекты maker для всех ваших тестов-передавать их в БД при тестировании уровня DAO, а также передавать их вызовам служб при тестировании бизнес-служб. Потому что производители многоразовые, его довольно сухой подход.
одна вещь, для которой вам все равно нужно будет использовать JMock, - это издеваться над вашим слоем DAO при тестировании вашего уровня обслуживания. Если ваша служба делает вызов DAO, вы должны убедиться, что он вводится с макетом вместо этого. Но вы все равно можете использовать своих создателей точно так же-когда настраиваете свои ожидания, просто убедитесь, что ваш издевательский DAO передает ожидаемый результат, используя Создателя для соответствующего объекта сущности. Таким образом, мы все еще не нарушают сухой.
хорошо написанные тесты уведомят вас, когда код изменится
мой последний совет, чтобы избежать вашей проблемы с изменением кода с течением времени, это всегда есть тест, который обращается к нулевым входам. Предположим, что при первом создании метода значения null неприемлемы. У вас должен быть тест, который проверяет, что исключение создается, если используется null. Если позднее значения null станут приемлемыми, код приложения может измениться так, что null значения обрабатываются по-новому, и исключение больше не создается. Когда это произойдет, ваш тест начнет терпеть неудачу, и у вас будет "голова вверх", что вещи не синхронизированы.
вам просто нужно решить, является ли возвращение null предполагаемой частью внешнего API или если это деталь реализации.
юнит-тесты не должны заботиться о деталях реализации.
Если это часть вашего предполагаемого внешнего API, то, поскольку ваше изменение потенциально нарушит клиентов, это, естественно, также должно нарушить модульный тест.
имеет ли смысл из внешнего POV, что эта вещь возвращает NULL или это удобное следствие, потому что в клиенте могут быть сделаны прямые предположения относительно значения этого NULL? Значение NULL должно означать void/nix/nada / unavailable без какого-либо другого значения.
Если вы планируете гранулировать это условие позже, то вы должны обернуть нулевую проверку во что-то, что возвращает информативное исключение, перечисление или явно названный bool.
одна из проблем с написанием модульных тестов заключается в том, что даже первые модульные тесты должны отражать полный API в конечном продукте. Вам нужно визуализировать полный API, а затем программировать против этого.
кроме того, вам нужно поддерживать ту же дисциплину в коде модульного теста, что и в производственном коде, избегая запахов дублирования и зависти к функциям.
для конкретного сценария вы меняете тип возврата метода, который будет пойман во время компиляции. Если бы это было не так, это появилось бы в покрытии кода (как упоминалось Аароном). Даже тогда вы должны иметь автоматические функциональные тесты, которые будут запускаться вскоре после регистрации. Тем не менее, я делаю автоматические тесты дыма, поэтому в моем случае те поймали бы это :).
не думая об этом, у вас все еще есть 2 важных фактора, играющих в начальном сценарии. Вы хотите чтобы уделить вашему коду модульного тестирования такое же внимание, как и остальному коду, что означает, что разумно хотеть сохранить их сухими. Если бы вы делали TDD, это даже подтолкнуло бы эту проблему к вашему дизайну в первую очередь. Если вы не в этом, другим противоположным фактором является YAGNI, вы не хотите получать каждый (un)вероятный сценарий в своем коде. Итак, для меня это было бы: если мои тесты говорят мне, что я чего-то не хватает, я дважды проверяю тест в порядке и продолжаю изменение. Я делаю конечно, не делать что, если сценарии с моими тестами, так как это ловушка.
Если я правильно понял вопрос, у вас есть бизнес-объект, который использует модель. Существует тест для взаимодействия между BO и моделью (тест A), и есть еще один тест, который проверяет взаимодействие между моделью и базой данных (тест B). Тест B изменяется, чтобы вернуть объект, но это изменение не влияет на тест A, потому что модель теста a высмеивается.
единственный способ сделать тест a неудачным, когда изменения теста B - не издеваться над моделью в тесте A и объединять два в один тест, что не очень хорошо, потому что вы будете тестировать слишком много (и вы используете разные фреймворки).
Если вы знаете об этой зависимости при написании тестов, я думаю, что приемлемым решением было бы оставить комментарий в каждом тесте, описывающем зависимость и как, если один изменяется, вам нужно изменить другой. В любом случае вам придется изменить тест B при рефакторинге, текущий тест завершится неудачей, как только вы внесете изменения.
ваш вопрос довольно запутанный, и количество текста не совсем помогает.
но смысл, который я мог бы извлечь через быстрое чтение, имеет мало смысла для меня, в том, что вы хотите, чтобы изменение без контракта повлияло на то, как работает макет.
издевательство-это средство, позволяющее вам сосредоточиться на тестировании определенной части системы. Издевательская часть всегда будет работать определенным образом, и тест может сосредоточиться на тестировании конкретной логики. Таким образом, вы не будет зависеть от несвязанной логики, проблем с задержкой, неожиданных данных и т. д.
У вас, вероятно, будет отдельное количество тестов, проверяющих издевательскую функциональность в другом контексте.
дело в том, что никакой связи не должно существовать между издевательским интерфейсом и реальной реализацией этого вообще. Это просто не имеет никакого смысла, так как вы издеваетесь над контрактом и даете ему свою собственную реализацию.
Я думаю, что ваша проблема нарушает принцип замены Лискова:
подтипы должны быть заменяемыми для их базовых типов
В идеале у вас будет класс, который зависит от абстракции. Абстракция, которая говорит:"Для того, чтобы работать, мне нужна реализация этого метода, который принимает этот параметр, возвращает этот результат, и если я делаю это неправильно, бросает мне это исключение". Все это будет определено на вашем интерфейсе, который вы зависит либо от ограничений по времени компиляции, либо от комментариев.
технически вы можете зависеть от абстракции, но в сценарии, который вы рассказываете, вы на самом деле не зависите от абстракции, вы на самом деле зависите от реализации. Вы говорите ,что"если этот метод изменит свое поведение, его пользователи сломаются, и мои тесты никогда не узнают". На уровне модульного теста вы правы. Но на контрактном уровне такое изменение поведения неправильно. Потому что, изменив метод, вы явно нарушают договор между вашим методом и его абонентами.
почему вы меняете метод? Ясно, что вызывающие этот метод теперь нуждаются в другом поведении. Итак, первое, что вы хотите сделать, это не изменить сам метод, а изменить абстракцию или контракт, от которого зависят ваши клиенты. Сначала они должны измениться и начать работать с новым контрактом: "хорошо, мои потребности изменились, я больше не хочу, чтобы этот метод возвращал это в этом конкретном сценарии, вместо этого разработчики этого интерфейса должны вернуть это". Итак, вы идете изменить свой интерфейс, вы идете изменить пользователей интерфейса по мере необходимости, и это включает в себя обновление своих тестов и последнее, что вы делаете, это изменение фактической реализации, которую вы передаете своим клиентам. Таким образом, вы не столкнетесь с ошибкой, о которой говорите.
и
class NeedsWork(IWorker b) { DoSth() { b.Work() }; }
...
AppBuilder() { INeedWork GetA() { return new NeedsWork(new Worker()); } }
- измените IWorker так, чтобы он отражал новые потребности NeedsWork.
- измените DoSth так, чтобы он работает с новой абстракцией, которая удовлетворяет его новые потребности.
- тест NeedsWork и убедитесь, что он работает с новым поведением.
- измените все реализации (Worker в этом сценарии), которые вы предоставляете для IWorker (что вы теперь пытаетесь сделать первым).
- работник теста так, что он соотвествует новым ожиданиям.
кажется страшным, но в реальной жизни это было бы тривиально для небольших изменений и болезненно для огромных изменений, поскольку это, на самом деле, должно быть.