Во многих случаях у меня может быть существующий класс с некоторым поведением:
class Lion
{
public void Eat(Herbivore herbivore) { ... }
}
... и у меня есть тестовый модуль ...
[TestMethod]
public void Lion_can_eat_herbivore()
{
var herbivore = buildHerbivoreForEating();
var test = BuildLionForTest();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
Теперь, что происходит, мне нужно создать класс Tiger с поведением, идентичным поведению Lion:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... и так как я хочу того же поведения, мне нужно запустить тот же тест, я делаю что-то вроде этого:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... и я рефакторинг моего теста:
[TestMethod]
public void Lion_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildLionForTest);
}
public void IHerbivoreEater_can_eat_herbivore(Func<IHerbivoreEater> builder)
{
var herbivore = buildHerbivoreForEating();
var test = builder();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
... а затем я добавляю еще один тест для моего нового Tiger
класса:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... а затем я выполняю рефакторинг моих Lion
и Tiger
классов (обычно по наследству, но иногда по составу):
class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }
abstract class HerbivoreEater : IHerbivoreEater
{
public void Eat(Herbivore herbivore) { ... }
}
... и все хорошо. Однако, поскольку функциональность теперь находится в HerbivoreEater
классе, теперь кажется, что что-то не так с тестами для каждого из этих поведений в каждом подклассе. Тем не менее, это подклассы, которые фактически потребляются, и это только детали реализации, в которых они имеют одинаковое поведение ( Lions
и Tigers
могут иметь, например, совершенно разные конечные применения).
Кажется излишним тестировать один и тот же код несколько раз, но есть случаи, когда подкласс может и не может переопределить функциональность базового класса (да, это может нарушить LSP, но давайте посмотрим правде в глаза, IHerbivoreEater
это просто удобный интерфейс тестирования - это может не иметь значения для конечного пользователя). Так что эти тесты действительно имеют ценность, я думаю.
Что другие люди делают в этой ситуации? Вы просто перемещаете свой тест в базовый класс или тестируете все подклассы на ожидаемое поведение?
РЕДАКТИРОВАТЬ :
Основываясь на ответе @pdr, я думаю, что мы должны рассмотреть это: IHerbivoreEater
это просто контракт подписи метода; это не определяет поведение. Например:
[TestMethod]
public void Tiger_eats_herbivore_haunches_first()
{
IHerbivoreEater_eats_herbivore_haunches_first(BuildTigerForTest);
}
[TestMethod]
public void Cheetah_eats_herbivore_haunches_first()
{
IHerbivoreEater_eats_herbivore_haunches_first(BuildCheetahForTest);
}
[TestMethod]
public void Lion_eats_herbivore_head_first()
{
IHerbivoreEater_eats_herbivore_head_first(BuildLionForTest);
}
источник
Animal
класса, который содержитEat
? Все животные едят, а потомуTiger
иLion
класс мог наследовать от животного.Eat
поведение в базовый класс, то все подклассы должны демонстрировать одинаковоеEat
поведение. Тем не менее, я говорю о 2 относительно не связанных классах, которые имеют общее поведение. Рассмотрим, например,Fly
поведениеBrick
иPerson
которое, как мы можем предположить, демонстрируют похожее поведение при полете, но не обязательно иметь смысл выводить их из общего базового класса.Ответы:
Это здорово, потому что показывает, как тесты действительно влияют на ваш взгляд на дизайн. Вы чувствуете проблемы в дизайне и задаете правильные вопросы.
Есть два способа посмотреть на это.
IHerbivoreEater - это контракт. Все IHerbivoreEaters должны иметь метод Eat, который принимает Herbivore. Теперь ваши тесты не заботятся о том, как их едят; Ваш Лев может начать с бедер, а Тигр - с горла. Все, что вас волнует, это то, что после вызова Eat травоядное съедается.
С другой стороны, часть того, что вы говорите, состоит в том, что все едоки IHerbivore едят травоядных точно так же (отсюда и базовый класс). В таком случае нет никакого смысла заключать договор с IHerbivoreEater. Ничего не предлагает. Вы также можете просто наследовать от HerbivoreEater.
Или, может быть, полностью избавиться от Льва и Тигра.
Но если Лев и Тигр отличаются во всех смыслах, кроме их привычек питания, то вам нужно задуматься, не столкнетесь ли вы с проблемами со сложным деревом наследования. Что делать, если вы также хотите извлечь оба класса из Feline или только класс Lion из KingOfItsDomain (возможно, вместе с Shark). Это где LSP действительно приходит.
Я бы предположил, что общий код лучше инкапсулирован.
То же самое касается тигра.
Вот, вот развивается прекрасная вещь (прекрасная, потому что я этого не собиралась). Если вы просто сделаете этот закрытый конструктор доступным для теста, вы можете передать поддельный IHerbivoreEatingStrategy и просто проверить, что сообщение передается инкапсулированному объекту должным образом.
А ваш сложный тест, о котором вы прежде всего беспокоились, должен только протестировать StandardHerbivoreEatingStrategy. Один класс, один набор тестов, без дублированного кода, о котором можно беспокоиться.
И если позже вы захотите сказать тиграм, что они должны есть своих травоядных по-другому, ни один из этих тестов не должен измениться. Вы просто создаете новую HerbivoreEatingStrategy и тестируете ее. Проводка проверяется на уровне интеграционных испытаний.
источник
IHerbivoreEater
это контракт, но только в той степени, в которой он мне нужен для тестирования. Мне кажется, что это один из случаев, когда печатание утки действительно поможет. Я просто хочу отправить их обоих в одну и ту же логику тестирования. Я не думаю, что интерфейс должен обещать поведение. Тесты должны сделать это.В общих чертах вы спрашиваете, целесообразно ли использовать знания белого ящика, чтобы пропустить некоторые тесты. С точки зрения черного ящика,
Lion
иTiger
разные классы. Таким образом, кто-то, не знакомый с кодом, может их протестировать, но вы, обладая глубокими знаниями в области реализации, знают, что вы можете просто протестировать одно животное.Одной из причин разработки модульных тестов является возможность рефакторинга позже, но с тем же интерфейсом черного ящика . Модульные тесты помогают вам убедиться, что ваши классы продолжают выполнять свой контракт с клиентами, или, по крайней мере, заставляет вас осознать и тщательно обдумать, как контракт может измениться. Вы сами понимаете это
Lion
илиTiger
могли бы переопределитьEat
в какой-то момент позже. Если это возможно удаленно, проведите простое модульное тестирование, которое может съесть каждое животное, которое вы поддерживаете, в виде:должно быть очень простым и достаточным, и вы сможете определить, когда объекты не соответствуют их контракту.
источник
Ты делаешь это правильно. Думайте о модульном тесте как о тесте поведения единственного использования вашего нового кода. Это тот же вызов, который вы сделали бы из производственного кода.
В этой ситуации вы совершенно правы; пользователь Lion или Tiger не будет (по крайней мере, не обязан) заботиться о том, что они оба HerbivoreEaters и что код, который фактически выполняется для метода, является общим для обоих в базовом классе. Точно так же, пользователю реферата HerbivoreEater (предоставляемого конкретным Львом или Тигром) не важно, какой он имеет. Их волнует, что их Lion, Tiger или неизвестная конкретная реализация HerbivoreEater будут правильно есть () Herbivore.
Итак, что вы в основном тестируете, так это то, что Лев будет есть как задумано, а тигр будет есть как задумано Важно проверить оба, потому что не всегда может быть правдой, что они едят одинаково; проверяя оба, вы гарантируете, что тот, который вы не хотели менять, не сделал. Поскольку оба они определены как HerbivoreEaters, по крайней мере до тех пор, пока вы не добавите Cheetah, вы также проверили, что все HerbivoreEaters будут питаться, как предполагалось. Ваше тестирование полностью охватывает и адекватно применяет код (при условии, что вы также сделаете все ожидаемые утверждения о том, что должно произойти из-за того, что HerbivoreEater съел травоядное животное).
источник