W wielu przypadkach mogę mieć istniejącą klasę z pewnym zachowaniem:
class Lion
{
public void Eat(Herbivore herbivore) { ... }
}
... i mam test jednostkowy ...
[TestMethod]
public void Lion_can_eat_herbivore()
{
var herbivore = buildHerbivoreForEating();
var test = BuildLionForTest();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
Teraz muszę stworzyć klasę Tygrys o identycznym zachowaniu jak Lew:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... a ponieważ chcę tego samego zachowania, muszę przeprowadzić ten sam test, robię coś takiego:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... i refaktoryzuję mój test:
[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);
}
... a następnie dodaję kolejny test dla mojej nowej Tigerklasy:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... a następnie refaktoryzuję swoje Lioni Tigerzajęcia (zwykle przez dziedziczenie, ale czasami przez kompozycję):
class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }
abstract class HerbivoreEater : IHerbivoreEater
{
public void Eat(Herbivore herbivore) { ... }
}
... i wszystko jest dobrze. Ponieważ jednak funkcjonalność jest teraz w HerbivoreEaterklasie, wydaje się, że jest coś nie tak z testowaniem każdego z tych zachowań w każdej podklasie. Jednak to właśnie podklasy są faktycznie konsumowane, a tylko szczegół implementacji pozwala im dzielić się zachowaniami (Lions i Tigersmoże mieć zupełnie różne zastosowania końcowe, na przykład).
Testowanie tego samego kodu wiele razy wydaje się zbędne, ale zdarzają się przypadki, w których podklasa może i zastępuje funkcjonalność klasy podstawowej (tak, może naruszać LSP, ale spójrzmy prawdzie w oczy, IHerbivoreEater to tylko wygodny interfejs testowy - to może nie mieć znaczenia dla użytkownika końcowego). Sądzę, że te testy mają pewną wartość.
Co robią inni ludzie w tej sytuacji? Czy po prostu przenosisz test do klasy podstawowej, czy testujesz wszystkie podklasy pod kątem oczekiwanego zachowania?
EDYTOWAĆ :
W oparciu o odpowiedź z @pdr uważam, że powinniśmy rozważyć: IHerbivoreEaterjest to tylko podpisanie umowy; nie określa zachowania. Na przykład:
[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);
}
Eatzachowanie w klasie bazowej, wszystkie podklasy powinny wykazywać to samo Eatzachowanie. Mówię jednak o 2 relatywnie niezwiązanych ze sobą klasach, które mają wspólne zachowanie. Weźmy na przykład, Flyzachowanie Bricki Personktóre możemy założyć, wykazują podobne zachowanie latanie, ale nie musi to mieć sens, aby je czerpać ze wspólnej klasy bazowej.
Animalklasy, która zawieraEat? Wszystkie zwierzęta jedzą, a zatemTigeriLionklasa mogą dziedziczyć po zwierzętach.