Во многих случаях у меня может быть существующий класс с некоторым поведением:
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);
}
Eat
поведение в базовый класс, то все подклассы должны демонстрировать одинаковое Eat
поведение. Тем не менее, я говорю о 2 относительно не связанных классах, которые имеют общее поведение. Рассмотрим, например, Fly
поведение Brick
и Person
которое, как мы можем предположить, демонстрируют похожее поведение при полете, но не обязательно иметь смысл выводить их из общего базового класса.
Animal
класса, который содержитEat
? Все животные едят, а потомуTiger
иLion
класс мог наследовать от животного.