Во многих случаях у меня может быть существующий класс с некоторым поведением:
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класс мог наследовать от животного.