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. SpeedLimitnecesita un Carcon un Motorpara hacer lo suyo. No está interesado en CarRadioabsoluto, 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

Motorprobablemente no debería tener ningunospeed. Debe tenerthrottleay calcular a entorquefunción de la corrienterpmythrottle. Es el trabajo del automóvil usar unTransmissionpara integrar eso en una velocidad actual, y convertirlo en unarpmseñal de retorno aMotor... Pero supongo que de todos modos no estabas en el realismo, ¿verdad?nullradio, 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
Carpueda existir sin aCarRadio. En cuyo caso, diría que no solo es aceptable pasar un valor nulo aCarRadioveces, sino que es obligatorio. Sus pruebas deben garantizar que la implementación de laCarclase sea robusta y no arroje excepciones de puntero nulo cuando noCarRadiohaya ninguna presente.Sin embargo, tomemos un ejemplo diferente: consideremos a
SteeringWheel. Supongamos que aCartiene que tener unSteeringWheel, pero a la prueba de velocidad realmente no le importa. En este caso, no pasaría un nuloSteeringWheelya que esto está empujando a laCarclase 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
Carno se puede construir un sin unSteeringWheel...Carsin 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? ?Carhubieran arrojado un NRE con un valor nuloCarRadio, pero el código relevante paraSpeedLimitno 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 delCarRadioargumento 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
CarRadioque sabe que no se usa en este caso, debe notar queCarRadiono se use se filtre en la prueba)Carsin especificar unCarRadioSospecho 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
CarRadioo
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.isViolatedBydebería estar aceptando una interfaz de rol que implementa el automóvil , en lugar de requerir un automóvil completo.IHaveSpeedes un nombre realmente pésimo; Esperemos que su idioma de dominio tenga una mejora.Con esa interfaz en su lugar, su prueba
SpeedLimitpodría ser mucho más simplefuente
IHaveSpeedinterfaz ya que (al menos en c #) no podría crear una instancia de una interfaz.No
De ningún modo. Por el contrario, si
NULLes 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. YNULLes 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
NULLincluso por el código mejorado es una buena prueba. Quisiera una prueba que muestre lo que sucede cuando pasoNULLcomo motor. Eso no es un olor a código, es lo opuesto a un olor a código. Está probando.fuente
Caraquí. Si bien no veo nada, consideraría una declaración incorrecta, la pregunta es sobre las pruebasSpeedLimit.NULLal constructor deCar.SpeedLimit. Puede ver que la prueba se llama "SpeedLimitTest" y la únicaAssertes 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
IMotortuviera ungetSpeed()método, tambiénDummyMotortendrí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
Carse compone de un conjunto deFeatures. Una característica puede ser unaCarRadio, oEntertainmentSystemque puede consistir en cosas como unaCarRadio,DvdPlayeretc.Entonces, como mínimo, tendrías que construir un
Carcon un conjunto deFeatures.EntertainmentSystemyCarRadioserían implementadores de la interfaz (Java, C #, etc.) delpublic [I|i]nterface Featurey esto sería una instancia del patrón de diseño Compuesto.Por lo tanto, siempre construiría un
CarconFeaturesy una prueba unitaria nunca crearía una instanciaCarsin ningunoFeatures. Aunque podría considerar burlarse de unBasicTestCarFeatureSetque tiene un conjunto mínimo de funcionalidades requeridas para su prueba más básica.Solo algunos pensamientos.
John
fuente