Comencé a escribir algunas pruebas unitarias para mi proyecto actual. Aunque realmente no tengo experiencia con eso. Primero quiero "obtenerlo" por completo, por lo que actualmente no estoy usando mi marco de trabajo IoC ni una biblioteca de imitación.
Me preguntaba si hay algo malo en proporcionar argumentos nulos a los constructores de objetos en las pruebas unitarias. Permítanme proporcionar un código de ejemplo:
public class CarRadio
{...}
public class Motor
{
public void SetSpeed(float speed){...}
}
public class Car
{
public Car(CarRadio carRadio, Motor motor){...}
}
public class SpeedLimit
{
public bool IsViolatedBy(Car car){...}
}
Otro ejemplo de código de automóvil (TM), reducido a solo las partes importantes para la pregunta. Ahora escribí una prueba algo como esto:
public class SpeedLimitTest
{
public void TestSpeedLimit()
{
Motor motor = new Motor();
motor.SetSpeed(10f);
Car car = new Car(null, motor);
SpeedLimit speedLimit = new SpeedLimit();
Assert.IsTrue(speedLimit.IsViolatedBy(car));
}
}
La prueba funciona bien. SpeedLimit
necesita un Car
con un Motor
para hacer lo suyo. No está interesado en CarRadio
absoluto, así que proporcioné nulo para eso.
Me pregunto si un objeto que proporciona la funcionalidad correcta sin estar completamente construido es una violación de SRP o un olor a código. Tengo la sensación persistente de que sí, pero speedLimit.IsViolatedBy(motor)
tampoco me parece bien: un automóvil, no un motor, viola un límite de velocidad. Tal vez solo necesito una perspectiva diferente para las pruebas unitarias en comparación con el código de trabajo, porque toda la intención es probar solo una parte del todo.
¿Construir objetos con nulos en pruebas unitarias huele a código?
fuente
Motor
probablemente no debería tener ningunospeed
. Debe tenerthrottle
ay calcular a entorque
función de la corrienterpm
ythrottle
. Es el trabajo del automóvil usar unTransmission
para integrar eso en una velocidad actual, y convertirlo en unarpm
señal de retorno aMotor
... Pero supongo que de todos modos no estabas en el realismo, ¿verdad?null
radio, el límite de velocidad se calcula correctamente. Ahora es posible que desee crear una prueba para validar el límite de velocidad con una radio; por si el comportamiento difiere ...Respuestas:
En el caso del ejemplo anterior, es razonable que a
Car
pueda existir sin aCarRadio
. En cuyo caso, diría que no solo es aceptable pasar un valor nulo aCarRadio
veces, sino que es obligatorio. Sus pruebas deben garantizar que la implementación de laCar
clase sea robusta y no arroje excepciones de puntero nulo cuando noCarRadio
haya ninguna presente.Sin embargo, tomemos un ejemplo diferente: consideremos a
SteeringWheel
. Supongamos que aCar
tiene que tener unSteeringWheel
, pero a la prueba de velocidad realmente no le importa. En este caso, no pasaría un nuloSteeringWheel
ya que esto está empujando a laCar
clase a lugares donde no está diseñada para ir. En este caso, sería mejor crear algún tipo deDefaultSteeringWheel
(para continuar con la metáfora) bloqueado en línea recta.fuente
Car
no se puede construir un sin unSteeringWheel
...Car
sin aCarRadio
, ¿no tendría sentido crear un constructor que no requiera que se pase uno y no tenga que preocuparse por un nulo explícito en absoluto? ?Car
hubieran arrojado un NRE con un valor nuloCarRadio
, pero el código relevante paraSpeedLimit
no lo haría. En el caso de la pregunta publicada ahora, estaría en lo correcto y, de hecho, lo haría.Sí. Tenga en cuenta la definición C2 del olor a código : "un CodeSmell es una pista de que algo podría estar mal, no una certeza".
Si está tratando de demostrar que eso
speedLimit.IsViolatedBy(car)
no depende delCarRadio
argumento pasado al constructor, entonces probablemente sería más claro para el futuro mantenedor hacer explícita esa restricción al introducir un doble de prueba que falla la prueba si se invoca.Si, como es más probable, solo está tratando de ahorrarse el trabajo de crear uno
CarRadio
que sabe que no se usa en este caso, debe notar queCarRadio
no se use se filtre en la prueba)Car
sin especificar unCarRadio
Sospecho fuertemente que el código está tratando de decirte que quieres un constructor que se vea como
y luego ese constructor puede decidir qué hacer con el hecho de que no hay
CarRadio
o
o
etc.
Ese es un segundo olor interesante: para probar SpeedLimit, debe construir un automóvil completo alrededor de una radio, aunque lo único que probablemente le interese a la comprobación de SpeedLimit es la velocidad .
Esta podría ser una pista que
SpeedLimit.isViolatedBy
debería estar aceptando una interfaz de rol que implementa el automóvil , en lugar de requerir un automóvil completo.IHaveSpeed
es un nombre realmente pésimo; Esperemos que su idioma de dominio tenga una mejora.Con esa interfaz en su lugar, su prueba
SpeedLimit
podría ser mucho más simplefuente
IHaveSpeed
interfaz ya que (al menos en c #) no podría crear una instancia de una interfaz.No
De ningún modo. Por el contrario, si
NULL
es un valor que está disponible como argumento (algunos lenguajes pueden tener construcciones que no permiten el paso de un puntero nulo), debe probarlo.Otra pregunta es qué sucede cuando pasas
NULL
. Pero eso es exactamente de lo que se trata una prueba unitaria: asegurarse de que un resultado definido suceda cuando para cada entrada posible. YNULL
es una entrada potencial, por lo que debe tener un resultado definido y una prueba para eso.Entonces, si se supone que su automóvil es construible sin una radio, debe escribir una prueba para eso. Si no es así y se supone que debe lanzar una excepción, debe escribir una prueba para eso.
De cualquier manera, sí, debes escribir una prueba
NULL
. Sería un olor a código si lo dejaras fuera. Eso significaría que tiene una rama de código no probada que acaba de asumir que mágicamente no tendrá errores.Tenga en cuenta que estoy de acuerdo con las otras respuestas de que su código bajo prueba podría mejorarse. No obstante, si el código mejorado tiene parámetros que son punteros, pasar
NULL
incluso por el código mejorado es una buena prueba. Quisiera una prueba que muestre lo que sucede cuando pasoNULL
como motor. Eso no es un olor a código, es lo opuesto a un olor a código. Está probando.fuente
Car
aquí. Si bien no veo nada, consideraría una declaración incorrecta, la pregunta es sobre las pruebasSpeedLimit
.NULL
al constructor deCar
.SpeedLimit
. Puede ver que la prueba se llama "SpeedLimitTest" y la únicaAssert
es verificar un método deSpeedLimit
. Las pruebasCar
, por ejemplo, "si se supone que su automóvil es construible sin una radio, debe escribir una prueba para eso", ocurriría en una prueba diferente.Yo personalmente dudaría en inyectar nulos. De hecho, cada vez que inyecto dependencias en un constructor, una de las primeras cosas que hago es generar una excepción ArgumentNullException si esas inyecciones son nulas. De esa manera puedo usarlos con seguridad dentro de mi clase, sin docenas de cheques nulos repartidos. Aquí hay un patrón muy común. Tenga en cuenta el uso de readonly para garantizar que las dependencias establecidas en el constructor no puedan establecerse como nulas (o cualquier otra cosa) después:
Con lo anterior, tal vez podría hacer una prueba unitaria o dos, donde inyecto nulos, solo para asegurarme de que mis cheques nulos funcionen como se esperaba.
Dicho esto, esto se convierte en un problema, una vez que haces dos cosas:
Entonces, para su ejemplo, tendría:
Puede encontrar más detalles sobre el uso de Moq aquí .
Por supuesto, si quiere comprenderlo sin burlarse primero, aún puede hacerlo, siempre que esté utilizando interfaces. Simplemente cree un CarRadio y un motor ficticios de la siguiente manera e inyéctelos:
fuente
IMotor
tuviera ungetSpeed()
método, tambiénDummyMotor
tendría que devolver una velocidad modificada después de un éxitosetSpeed
.No solo está bien, es una técnica conocida de ruptura de dependencia para obtener código heredado en un arnés de prueba. (Ver "Trabajando efectivamente con código heredado" por Michael Feathers).
¿Huele? Bien...
La prueba no huele. La prueba está haciendo su trabajo. Está declarando que "este objeto puede ser nulo y el código bajo prueba aún funciona correctamente".
El olor está en la implementación. Al declarar un argumento de constructor, estás declarando que "esta clase necesita esto para funcionar correctamente". Obviamente, eso no es cierto. Cualquier cosa falsa es un poco maloliente.
fuente
Retrocedamos a los primeros principios.
Sin embargo, habrá constructores o métodos que toman otras clases como parámetros. La forma en que maneje esto variará de un caso a otro, pero la configuración debe ser mínima ya que son tangenciales para el objetivo. Puede haber escenarios en los que pasar un parámetro NULL es perfectamente válido, como un automóvil que no tiene radio.
Asumo aquí, que solo estamos probando la línea de carrera para comenzar (para continuar con el tema del automóvil), pero también es posible que desee pasar NULL a otros parámetros para verificar que cualquier código defensivo y mecanismos de manejo de excepciones funcionen como necesario.
Hasta aquí todo bien. Sin embargo, ¿qué pasa si NULL no es un valor válido? Bueno, aquí estás en el mundo turbio de simulacros, trozos, muñecos y falsificaciones .
Si todo esto parece un poco difícil de asumir, en realidad ya está utilizando un ficticio al pasar NULL en el constructor de automóviles, ya que es un valor que potencialmente no se utilizará.
Lo que debería quedar claro con bastante rapidez, es que potencialmente está disparando su código con una gran cantidad de manejo NULL. Si reemplaza los parámetros de clase con interfaces, también allana el camino para burlarse y DI, lo que hace que estos sean mucho más fáciles de manejar.
fuente
Echando un vistazo a su diseño, sugeriría que a
Car
se compone de un conjunto deFeatures
. Una característica puede ser unaCarRadio
, oEntertainmentSystem
que puede consistir en cosas como unaCarRadio
,DvdPlayer
etc.Entonces, como mínimo, tendrías que construir un
Car
con un conjunto deFeatures
.EntertainmentSystem
yCarRadio
serían implementadores de la interfaz (Java, C #, etc.) delpublic [I|i]nterface Feature
y esto sería una instancia del patrón de diseño Compuesto.Por lo tanto, siempre construiría un
Car
conFeatures
y una prueba unitaria nunca crearía una instanciaCar
sin ningunoFeatures
. Aunque podría considerar burlarse de unBasicTestCarFeatureSet
que tiene un conjunto mínimo de funcionalidades requeridas para su prueba más básica.Solo algunos pensamientos.
John
fuente