Можно ли подделывать часть тестируемого класса?

22

Предположим, у меня есть класс (простите за надуманный пример и плохой дизайн):

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 ()?

пользователь
источник
Если вы одновременно тестируете некоторый код, как это может быть плохо? В чем проблема? Обычно лучше тестировать код чаще и интенсивнее.
пользователь неизвестен
Только что узнал о вашем обновлении, я обновил свой ответ
Уинстон Эверт

Ответы:

27

Вы должны разбить класс под вопросом.

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

Не обращая внимания на глупость этого дизайна:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

ОБНОВИТЬ

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

Тот факт, что FullName использует FirstName и LastName, является деталью реализации. Ничто вне класса не должно волновать, что это правда. Посмеиваясь над публичными методами для тестирования объекта, вы делаете предположение о том, что объект реализован.

В какой-то момент в будущем это предположение может перестать быть верным. Возможно, вся логика имени будет перемещена в объект Name, который Person просто вызывает. Возможно, FullName будет напрямую обращаться к переменным-членам first_name и last_name, а не вызывать FirstName и LastName.

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

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

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

Единственный случай, когда кажется, что методы, которые FullName использует для имитации, имеет смысл, если каким-то образом FirstName () и LastName () были нетривиальными операциями. Возможно, вы пишете один из этих генераторов случайных имен, или FirstName и LastName запрашивают у базы данных ответ, или что-то в этом роде. Но если это то, что происходит, это говорит о том, что объект делает что-то, что не принадлежит классу Person.

Иными словами, насмешка над методами - это взятие объекта и разбивание на две части. Одна часть подвергается насмешкам, в то время как другая часть испытывается. То, что вы делаете, - это, по сути, случайное разрушение объекта. Если это так, просто разбейте объект.

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

ОБНОВЛЕНИЕ СНОВА

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

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

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

Но какая разница? Если FirstName () и LastName () являются членами другого объекта, действительно ли это изменит проблему FullName ()? Если мы решим, что необходимо издеваться над FirstName, а LastName действительно помогает им оказаться на другом объекте?

Я думаю, что если вы используете ваш насмешливый подход, то вы создаете шов на объекте. У вас есть такие функции, как FirstName () и LastName (), которые напрямую взаимодействуют с внешним источником данных. У вас также есть FullName (), который не имеет. Но так как они все в одном классе, это не очевидно. Некоторые части не должны иметь прямой доступ к источнику данных, а другие. Ваш код будет понятнее, если просто разбить эти две группы.

РЕДАКТИРОВАТЬ

Давайте сделаем шаг назад и спросим: почему мы высмеиваем объекты при тестировании?

  1. Сделайте тесты запущенными последовательно (избегайте доступа к вещам, которые меняются от запуска к запуску)
  2. Избегайте доступа к дорогостоящим ресурсам (не обращайтесь к сторонним сервисам и т. Д.)
  3. Упростить тестируемую систему
  4. Упростите тестирование всех возможных сценариев (например, имитация сбоя и т. Д.)
  5. Избегайте зависимости от деталей других частей кода, чтобы изменения в этих других частях кода не нарушали этот тест.

Теперь, я думаю, причины 1-4 не относятся к этому сценарию. Насмешка внешнего источника при проверке полного имени устраняет все эти причины насмешек. Единственная не обработанная часть - это простота, но кажется, что объект достаточно прост, и это не проблема.

Я думаю, что вы беспокоитесь о причине номер 5. Проблема заключается в том, что в какой-то момент в будущем изменение реализации FirstName и LastName нарушит тест. В будущем FirstName и LastName могут получать имена из другого местоположения или источника. Но FullName, вероятно, всегда будет FirstName() + " " + LastName(). Вот почему вы хотите протестировать FullName, высмеивая FirstName и LastName.

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

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

Я подозреваю, что вы можете возражать против разделения вашего объекта, но почему?

РЕДАКТИРОВАТЬ

Я был неправ.

Вы должны разбивать объекты, а не вводить произвольные разбиения, издеваясь над отдельными методами. Тем не менее, я был слишком сосредоточен на одном методе расщепления объектов. Однако OO предоставляет несколько методов разбиения объекта.

Что бы я предложил:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

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

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

Уинстон Эверт
источник
8
Хорошо сказано. Если вы пытаетесь найти обходные пути в тестировании, вам, вероятно, нужно пересмотреть свой дизайн (как примечание, у меня теперь голос Фрэнка Синатры застрял в моей голове).
проблематично
2
«Насмехаясь над публичными методами для проверки объекта, вы делаете предположение о том, [как] этот объект реализован». Но разве это не так, когда вы заглушаете объект? Например, предположим, я знаю, что мой метод xyz () вызывает какой-то другой объект. Чтобы проверить xyz () изолированно, я должен заглушить другой объект. Поэтому мой тест должен знать о деталях реализации моего метода xyz ().
Пользователь
В моем случае методы «FirstName ()» и «LastName ()» просты, но они запрашивают API сторонних производителей для их результата.
Пользователь
@ Пользователь, обновленный, наверняка вы издеваетесь над сторонним API для тестирования FirstName и LastName. Что плохого в том, чтобы делать такие же насмешки при тестировании FullName?
Уинстон Эверт
@Winston, в этом вся моя суть, в настоящее время я высмеиваю сторонний API, используемый в имени и фамилии, чтобы проверить fullname (), но я бы предпочел не заботиться о том, как имя и фамилия реализованы при тестировании полного имени (конечно, это нормально, когда я проверяю имя и фамилию). Отсюда и мой вопрос о насмешливом имени и фамилии.
Пользователь
10

Хотя я согласен с ответом Уинстона Эверта, иногда просто невозможно разбить класс, будь то из-за нехватки времени, из-за того, что API уже используется где-то еще, или что у вас есть.

В таком случае вместо методов насмешки я бы написал свои тесты, чтобы постепенно покрывать класс. Проверьте работу getExpenses()и getRevenue()методы, а затем проверить getProfit()методы, а затем проверить общие методы. Это правда, что у вас будет более одного теста, охватывающего определенный метод, но, поскольку вы написали прохождение тестов, чтобы охватить методы по отдельности, вы можете быть уверены, что ваши результаты надежны, учитывая протестированные данные.

проблематичный
источник
1
Согласовано. Но только для того, чтобы подчеркнуть мысль для любого, кто читает это, это решение, которое вы используете, если не можете сделать лучшее решение.
Уинстон Эверт
5
@Winston, и это решение, которое вы используете, прежде чем найти лучшее решение. То есть, имея большой шарик унаследованного кода, вы должны покрыть его модульными тестами, прежде чем сможете выполнить его рефакторинг.
Петер Тёрёк
@ Петер Тёрёк, хорошая мысль. Хотя я думаю, что это подпадает под «не может сделать лучшее решение». :)
Уинстон Эверт
@all Тестирование метода getProfit () будет очень трудным, поскольку различные сценарии для getExpenses () и getRevenues () фактически умножают сценарии, необходимые для getProfit (). Если мы тестируем getExpenses () и получаем Revenues () независимо друг от друга, разве не стоит насмехаться над этими методами для тестирования getProfit ()?
Мохд Фарид
7

Чтобы еще больше упростить ваши примеры, скажем, вы тестируете C(), что зависит от того, A()и у B()каждого из которых есть свои зависимости. ИМО все сводится к тому, что ваш тест пытается достичь.

Если вы тестируете поведение C()данного известного поведения, A()а B()затем, вероятно, проще всего и лучше оцепить A()и B(). Вероятно, это будет называться юнит-тестом пуристов.

Если вы тестируете поведение всей системы ( C()с точки зрения), я бы оставил A()и B()в том виде, в каком он реализован, или либо потушу их зависимости (если это позволяет проводить тестирование), либо настрою среду песочницы, такую ​​как тест база данных. Я бы назвал это интеграционным тестом.

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

Ребекка Скотт
источник