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 Tiger
clase:
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... y luego refactorizo my Lion
and Tiger
classes (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 HerbivoreEater
clase, 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 ( Lions
y Tigers
pueden 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, IHerbivoreEater
es 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: IHerbivoreEater
es 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);
}
fuente
Animal
clase que contieneEat
? Todos los animales comen y, por lo tanto, la claseTiger
yLion
podría heredar de los animales.Eat
comportamiento en la clase base, entonces todas las subclases deberían exhibir el mismoEat
comportamiento. Sin embargo, estoy hablando de 2 clases relativamente no relacionadas que comparten un comportamiento. Considere, por ejemplo, elFly
comportamiento deBrick
yPerson
que, podemos suponer, exhibe un comportamiento de vuelo similar, pero no necesariamente tiene sentido que se deriven de una clase base común.Respuestas:
Esto es genial porque muestra cómo las pruebas realmente impulsan su forma de pensar sobre el diseño. Está detectando problemas en el diseño y haciendo las preguntas correctas.
Hay dos formas de ver esto.
IHerbivoreEater es un contrato. Todos los IHerbivoreEaters deben tener un método Eat que acepte un Herbivore. Ahora, a sus pruebas no les importa cómo se comen; tu León podría comenzar con las ancas y el Tigre podría comenzar en la garganta. Lo único que le importa a su prueba es que después de llamar a Eat, se coma al herbívoro.
Por otro lado, parte de lo que está diciendo es que todos los comedores de herbívoros comen al herbívoro exactamente de la misma manera (de ahí la clase base). Siendo ese el caso, no tiene sentido tener un contrato IHerbivoreEater en absoluto. No ofrece nada. También puedes heredar de HerbivoreEater.
O tal vez acabar con Lion and Tiger por completo.
Pero, si Lion y Tiger son diferentes en todos los sentidos, aparte de sus hábitos alimenticios, entonces debe comenzar a preguntarse si va a tener problemas con un árbol de herencia complejo. ¿Qué sucede si también desea derivar ambas clases de Feline, o solo la clase Lion de KingOfItsDomain (junto con Shark, tal vez)? Aquí es donde realmente entra LSP.
Sugeriría que el código común está mejor encapsulado.
Lo mismo va para Tiger.
Ahora, aquí hay algo hermoso en desarrollo (hermoso porque no tenía la intención). Si solo hace que ese constructor privado esté disponible para la prueba, puede pasar una IHerbivoreEatingStrategy falsa y simplemente probar que el mensaje se pasa al objeto encapsulado correctamente.
Y su prueba compleja, la que le preocupaba en primer lugar, solo tiene que probar la estrategia StandardHerbivoreEatingStrategy. Una clase, un conjunto de pruebas, sin código duplicado de qué preocuparse.
Y si, más adelante, quieres decirle a los Tigres que deberían comer sus herbívoros de una manera diferente, ninguna de estas pruebas tiene que cambiar. Simplemente está creando una nueva estrategia HerbivoreEatingStrategy y está probando eso. El cableado se prueba a nivel de prueba de integración.
fuente
IHerbivoreEater
es un contrato, pero solo en la medida en que lo necesito para probar. Me parece que este es un caso donde la escritura de patos realmente ayudaría. Solo quiero enviarlos a ambos a la misma lógica de prueba ahora. No creo que la interfaz deba prometer el comportamiento. Las pruebas deberían hacer eso.De manera indirecta, se pregunta si es apropiado utilizar el conocimiento de la caja blanca para omitir algunas pruebas. Desde una perspectiva de caja negra,
Lion
yTiger
son diferentes clases. Entonces, alguien que no esté familiarizado con el código los probaría, pero usted con un conocimiento profundo de implementación sabe que puede salirse con la suya simplemente probando un animal.Parte de la razón para desarrollar pruebas unitarias es permitirte refactorizar más tarde pero mantener la misma interfaz de caja negra . Las pruebas unitarias lo ayudan a garantizar que sus clases continúen cumpliendo con su contrato con los clientes, o al menos lo obliga a darse cuenta y pensar cuidadosamente cómo podría cambiar el contrato. Usted mismo se da cuenta de eso
Lion
oTiger
puede anularloEat
en algún momento posterior. Si esto es remotamente posible, una simple prueba de prueba unitaria que cada animal que apoya puede comer, de la siguiente manera:debe ser muy simple de hacer y suficiente y garantizará que pueda detectar cuándo los objetos no cumplen con su contrato.
fuente
Lo estás haciendo bien. Piense en una prueba unitaria como la prueba del comportamiento de un solo uso de su nuevo código. Esta es la misma llamada que haría desde el código de producción.
En esa situación, tienes toda la razón; un usuario de un León o un Tigre no (al menos no tendrá que preocuparse) de que ambos sean HerbivoreEaters y que el código que realmente se ejecuta para el método sea común para ambos en la clase base. Del mismo modo, a un usuario de un resumen HerbivoreEater (proporcionado por el León o el Tigre concreto) no le importará lo que tenga. Lo que les importa es que su implementación de León, Tigre o concreto desconocido de HerbivoreEater se comerá () un herbívoro correctamente.
Entonces, lo que básicamente está probando es que un león comerá según lo previsto, y que un tigre comerá según lo previsto. Es importante probar ambos, porque no siempre es cierto que ambos comen exactamente de la misma manera; Al probar ambos, se asegura de que el que no desea cambiar, no lo hizo. Debido a que ambos son los HerbivoreEaters definidos, al menos hasta que agregue Cheetah, también ha probado que todos los HerbivoreEaters comerán según lo previsto. Su prueba cubre completamente y ejercita adecuadamente el código (siempre que también haga todas las afirmaciones esperadas de lo que debería resultar de un HerbivoreEater comiendo un Herbivore).
fuente