У багатьох випадках у мене може бути існуючий клас з деякою поведінкою:
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);
}
Тепер, що трапляється, мені потрібно створити клас Тигра з ідентичною поведінкою, як у Лева:
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клас може успадкувати від тварини.