En muchos casos, podría tener una clase existente con algún comportamiento:
class Lion
{
public void Eat(Herbivore herbivore) { ... }
}
... y tengo una prueba unitaria ...
[TestMethod]
public void Lion_can_eat_herbivore()
{
var herbivore = buildHerbivoreForEating();
var test = BuildLionForTest();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
Ahora, lo que sucede es que necesito crear una clase Tiger con un comportamiento idéntico al del León:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... y como quiero el mismo comportamiento, necesito ejecutar la misma prueba, hago algo como esto:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... y refactorizo mi prueba:
[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);
}
... y luego agrego otra prueba para mi nueva Tigerclase:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... y luego refactorizo my Lionand Tigerclasses (generalmente por herencia, pero a veces por composición):
class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }
abstract class HerbivoreEater : IHerbivoreEater
{
public void Eat(Herbivore herbivore) { ... }
}
... y todo está bien. Sin embargo, dado que la funcionalidad ahora está en la HerbivoreEaterclase, ahora parece que hay algo malo en tener pruebas para cada uno de estos comportamientos en cada subclase. Sin embargo, son las subclases las que realmente se están consumiendo, y es solo un detalle de implementación que comparten comportamientos superpuestos ( Lionsy Tigerspueden tener usos finales totalmente diferentes, por ejemplo).
Parece redundante probar el mismo código varias veces, pero hay casos en los que la subclase puede anular la funcionalidad de la clase base y lo hace (sí, podría violar el LSP, pero admitámoslo, IHerbivoreEateres solo una interfaz de prueba conveniente: es puede no ser importante para el usuario final). Entonces, estas pruebas tienen algún valor, creo.
¿Qué hacen otras personas en esta situación? ¿Simplemente mueve su prueba a la clase base, o prueba todas las subclases para el comportamiento esperado?
EDITAR :
Basado en la respuesta de @pdr, creo que deberíamos considerar esto: IHerbivoreEateres solo un contrato de firma de método; No especifica el comportamiento. Por ejemplo:
[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);
}
Eatcomportamiento en la clase base, entonces todas las subclases deberían exhibir el mismo Eatcomportamiento. Sin embargo, estoy hablando de 2 clases relativamente no relacionadas que comparten un comportamiento. Considere, por ejemplo, el Flycomportamiento de Bricky Personque, podemos suponer, exhibe un comportamiento de vuelo similar, pero no necesariamente tiene sentido que se deriven de una clase base común.
Animalclase que contieneEat? Todos los animales comen y, por lo tanto, la claseTigeryLionpodría heredar de los animales.