Предположим, у меня есть класс (простите за надуманный пример и плохой дизайн):
class MyProfit
{
public decimal GetNewYorkRevenue();
public decimal GetNewYorkExpenses();
public decimal GetNewYorkProfit();
public decimal GetMiamiRevenue();
public decimal GetMiamiExpenses();
public decimal GetMiamiProfit();
public bool BothCitiesProfitable();
}
(Обратите внимание, что методы GetxxxRevenue () и GetxxxExpenses () имеют ограниченные зависимости)
Теперь я тестирую модуль BothCitiesProfitable (), который зависит от GetNewYorkProfit () и GetMiamiProfit (). Это нормально, чтобы заглушки GetNewYorkProfit () и GetMiamiProfit ()?
Кажется, что если я этого не сделаю, то я одновременно тестирую GetNewYorkProfit () и GetMiamiProfit () вместе с BothCitiesProfitable (). Мне нужно убедиться, что я настроил заглушки для GetxxxRevenue () и GetxxxExpenses (), чтобы методы GetxxxProfit () возвращали правильные значения.
До сих пор я видел только пример заглушки зависимостей от внешних классов, а не от внутренних методов.
И если все в порядке, есть ли определенный шаблон, который я должен использовать для этого?
ОБНОВИТЬ
Я обеспокоен тем, что мы можем упустить основную проблему, и это, вероятно, вина моего плохого примера. Фундаментальный вопрос: если метод в классе имеет зависимость от другого открытого метода в этом же классе, можно ли (или даже рекомендуется) заглушить этот другой метод?
Может быть, я что-то упускаю, но я не уверен, что разделение класса всегда имеет смысл. Возможно, еще один минимально лучший пример:
class Person
{
public string FirstName()
public string LastName()
public string FullName()
}
где полное имя определяется как:
public string FullName()
{
return FirstName() + " " + LastName();
}
Это нормально для заглушки FirstName () и LastName () при тестировании FullName ()?
источник
Ответы:
Вы должны разбить класс под вопросом.
Каждый класс должен выполнить простую задачу. Если ваша задача слишком сложна для тестирования, то задача, которую выполняет класс, слишком велика.
Не обращая внимания на глупость этого дизайна:
ОБНОВИТЬ
Проблема с методами-заглушками в классе заключается в том, что вы нарушаете инкапсуляцию. Ваш тест должен проверять, соответствует ли внешнее поведение объекта спецификациям. Что бы ни происходило внутри объекта, это не его дело.
Тот факт, что FullName использует FirstName и LastName, является деталью реализации. Ничто вне класса не должно волновать, что это правда. Посмеиваясь над публичными методами для тестирования объекта, вы делаете предположение о том, что объект реализован.
В какой-то момент в будущем это предположение может перестать быть верным. Возможно, вся логика имени будет перемещена в объект Name, который Person просто вызывает. Возможно, FullName будет напрямую обращаться к переменным-членам first_name и last_name, а не вызывать FirstName и LastName.
Второй вопрос: почему вы чувствуете необходимость сделать это? В конце концов, ваш класс может быть протестирован примерно так:
Вы не должны чувствовать необходимость что-либо заглушки для этого примера. Если вы это сделаете, то вы счастливы и здоровы ... остановите это! Нет смысла издеваться над этими методами, потому что у вас все равно есть контроль над тем, что в них.
Единственный случай, когда кажется, что методы, которые FullName использует для имитации, имеет смысл, если каким-то образом FirstName () и LastName () были нетривиальными операциями. Возможно, вы пишете один из этих генераторов случайных имен, или FirstName и LastName запрашивают у базы данных ответ, или что-то в этом роде. Но если это то, что происходит, это говорит о том, что объект делает что-то, что не принадлежит классу Person.
Иными словами, насмешка над методами - это взятие объекта и разбивание на две части. Одна часть подвергается насмешкам, в то время как другая часть испытывается. То, что вы делаете, - это, по сути, случайное разрушение объекта. Если это так, просто разбейте объект.
Если у вас простой урок, вы не должны испытывать необходимость его макетировать во время теста. Если ваш класс достаточно сложен, и вы чувствуете необходимость издеваться над ним, вы должны разбить его на более простые части.
ОБНОВЛЕНИЕ СНОВА
На мой взгляд, объект имеет внешнее и внутреннее поведение. Внешнее поведение включает вызовы значений в другие объекты и т. Д. Очевидно, что все в этой категории должно быть проверено. (иначе, что бы вы проверили?) Но внутреннее поведение не должно быть проверено.
Теперь внутреннее поведение проверяется, потому что именно оно приводит к внешнему поведению. Но я не пишу тесты непосредственно на внутреннее поведение, только косвенно через внешнее поведение.
Если я хочу что-то проверить, я полагаю, что это нужно переместить, чтобы оно стало внешним поведением. Вот почему я думаю, что если вы хотите что-то издеваться, вы должны разделить объект так, чтобы вещь, которую вы хотите высмеять, теперь была во внешнем поведении рассматриваемых объектов.
Но какая разница? Если FirstName () и LastName () являются членами другого объекта, действительно ли это изменит проблему FullName ()? Если мы решим, что необходимо издеваться над FirstName, а LastName действительно помогает им оказаться на другом объекте?
Я думаю, что если вы используете ваш насмешливый подход, то вы создаете шов на объекте. У вас есть такие функции, как FirstName () и LastName (), которые напрямую взаимодействуют с внешним источником данных. У вас также есть FullName (), который не имеет. Но так как они все в одном классе, это не очевидно. Некоторые части не должны иметь прямой доступ к источнику данных, а другие. Ваш код будет понятнее, если просто разбить эти две группы.
РЕДАКТИРОВАТЬ
Давайте сделаем шаг назад и спросим: почему мы высмеиваем объекты при тестировании?
Теперь, я думаю, причины 1-4 не относятся к этому сценарию. Насмешка внешнего источника при проверке полного имени устраняет все эти причины насмешек. Единственная не обработанная часть - это простота, но кажется, что объект достаточно прост, и это не проблема.
Я думаю, что вы беспокоитесь о причине номер 5. Проблема заключается в том, что в какой-то момент в будущем изменение реализации FirstName и LastName нарушит тест. В будущем FirstName и LastName могут получать имена из другого местоположения или источника. Но FullName, вероятно, всегда будет
FirstName() + " " + LastName()
. Вот почему вы хотите протестировать FullName, высмеивая FirstName и LastName.То, что у вас есть, это некоторое подмножество объекта персонажа, которое может измениться больше, чем другие. Остальная часть объекта использует это подмножество. Это подмножество в настоящее время извлекает свои данные, используя один источник, но может извлечь эти данные совершенно другим способом на более позднем этапе. Но для меня это звучит так, будто это подмножество - особый объект, пытающийся выбраться.
Мне кажется, что если вы издеваетесь над методом объекта, вы разделяете его на части. Но вы делаете это на специальной основе. Ваш код не дает понять, что внутри вашего объекта Person есть две отдельные части. Просто разделите этот объект в вашем реальном коде, чтобы из чтения кода было понятно, что происходит. Выберите фактическое разделение объекта, которое имеет смысл, и не пытайтесь разделить объект по-разному для каждого теста.
Я подозреваю, что вы можете возражать против разделения вашего объекта, но почему?
РЕДАКТИРОВАТЬ
Я был неправ.
Вы должны разбивать объекты, а не вводить произвольные разбиения, издеваясь над отдельными методами. Тем не менее, я был слишком сосредоточен на одном методе расщепления объектов. Однако OO предоставляет несколько методов разбиения объекта.
Что бы я предложил:
Может быть, это то, что вы делали все это время. Но я не думаю, что этот метод будет иметь проблемы, которые я видел с методами насмешки, потому что мы четко определили, на чьей стороне находится каждый метод. И используя наследование, мы избегаем неловкости, которая возникнет, если мы будем использовать дополнительный объект-обертку.
Это вносит некоторую сложность, и только для пары служебных функций я, вероятно, просто протестирую их, высмеивая основной сторонний источник. Конечно, им грозит повышенная опасность взлома, но переставлять их не стоит. Если у вас есть достаточно сложный объект, который вам нужно разделить, я думаю, что-то вроде этого - хорошая идея.
источник
Хотя я согласен с ответом Уинстона Эверта, иногда просто невозможно разбить класс, будь то из-за нехватки времени, из-за того, что API уже используется где-то еще, или что у вас есть.
В таком случае вместо методов насмешки я бы написал свои тесты, чтобы постепенно покрывать класс. Проверьте работу
getExpenses()
иgetRevenue()
методы, а затем проверитьgetProfit()
методы, а затем проверить общие методы. Это правда, что у вас будет более одного теста, охватывающего определенный метод, но, поскольку вы написали прохождение тестов, чтобы охватить методы по отдельности, вы можете быть уверены, что ваши результаты надежны, учитывая протестированные данные.источник
Чтобы еще больше упростить ваши примеры, скажем, вы тестируете
C()
, что зависит от того,A()
и уB()
каждого из которых есть свои зависимости. ИМО все сводится к тому, что ваш тест пытается достичь.Если вы тестируете поведение
C()
данного известного поведения,A()
аB()
затем, вероятно, проще всего и лучше оцепитьA()
иB()
. Вероятно, это будет называться юнит-тестом пуристов.Если вы тестируете поведение всей системы (
C()
с точки зрения), я бы оставилA()
иB()
в том виде, в каком он реализован, или либо потушу их зависимости (если это позволяет проводить тестирование), либо настрою среду песочницы, такую как тест база данных. Я бы назвал это интеграционным тестом.Оба подхода могут быть действительными, поэтому, если он спроектирован так, что вы можете протестировать его любым способом, он, вероятно, будет лучше в долгосрочной перспективе.
источник