Dans de nombreux cas, je pourrais avoir une classe existante avec un certain comportement:
class Lion
{
public void Eat(Herbivore herbivore) { ... }
}
... et j'ai un test unitaire ...
[TestMethod]
public void Lion_can_eat_herbivore()
{
var herbivore = buildHerbivoreForEating();
var test = BuildLionForTest();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
Maintenant, ce qui se passe, c'est que je dois créer une classe Tiger avec un comportement identique à celui du Lion:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... et comme je veux le même comportement, je dois faire le même test, je fais quelque chose comme ça:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... et je refactorise mon 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);
}
... puis j'ajoute un autre test pour ma nouvelle Tiger
classe:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... puis je refactorise mes classes Lion
et Tiger
(généralement par héritage, mais parfois par composition):
class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }
abstract class HerbivoreEater : IHerbivoreEater
{
public void Eat(Herbivore herbivore) { ... }
}
... et tout va bien. Cependant, puisque la fonctionnalité est maintenant dans la HerbivoreEater
classe, il semble maintenant qu'il y ait quelque chose de mal à avoir des tests pour chacun de ces comportements dans chaque sous-classe. Pourtant, ce sont les sous-classes qui sont réellement consommées, et ce n'est qu'un détail de mise en œuvre qui partage des comportements qui se chevauchent ( Lions
et Tigers
peuvent avoir des utilisations finales totalement différentes, par exemple).
Il semble redondant de tester le même code plusieurs fois, mais il y a des cas où la sous-classe peut et remplace la fonctionnalité de la classe de base (oui, cela pourrait violer le LSP, mais avouons-le, IHerbivoreEater
n'est qu'une interface de test pratique - elle peut ne pas avoir d'importance pour l'utilisateur final). Ces tests ont donc une certaine valeur, je pense.
Que font les autres dans cette situation? Déplacez-vous simplement votre test vers la classe de base ou testez-vous toutes les sous-classes pour le comportement attendu?
MODIFIER :
Sur la base de la réponse de @pdr, je pense que nous devrions considérer ceci: IHerbivoreEater
c'est juste un contrat de signature de méthode; il ne spécifie pas de comportement. Par exemple:
[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
comportement dans la classe de base, toutes les sous-classes doivent présenter le même Eat
comportement. Cependant, je parle de 2 classes relativement indépendantes qui partagent un comportement. Considérons, par exemple, le Fly
comportement de Brick
et Person
qui, nous pouvons supposer, présentent un comportement de vol similaire, mais cela n'a pas nécessairement de sens de les faire dériver d'une classe de base commune.
Animal
classe qui contientEat
? Tous les animaux mangent, et donc la classeTiger
etLion
pourrait hériter de l'animal.