Este es un extracto del eBook de Unit Testing Succinctly, por Marc Clifton, amablemente proporcionado por Syncfusion.
Los artículos anteriores han abordado una variedad de preocupaciones y beneficios de las pruebas unitarias. Este artículo es un análisis más formal del costo y los beneficios de las pruebas unitarias..
Su código de prueba de unidad es una entidad separada del código que se está probando, sin embargo, comparte muchos de los mismos problemas requeridos por su código de producción:
Además, las pruebas unitarias también pueden:
Al determinar si las pruebas se pueden escribir con un solo método, se debe considerar:
Uno debe darse cuenta de que el recuento de líneas del código para probar incluso un método simple podría ser considerablemente mayor que el recuento de líneas del propio método..
Cambiar el código de producción a menudo puede invalidar las pruebas unitarias. Los cambios de código se dividen aproximadamente en dos categorías:
El primero generalmente tiene poco o ningún requisito de mantenimiento en las pruebas unitarias existentes. Este último, sin embargo, a menudo requiere una revisión considerable de las pruebas unitarias, dependiendo de la complejidad del cambio:
Como se mencionó anteriormente, las pruebas unitarias, especialmente en un proceso guiado por pruebas, imponen ciertos paradigmas de implementación y arquitectura mínima. Para apoyar aún más la facilidad de configuración o eliminación de algunas áreas del código, las pruebas unitarias también pueden beneficiarse de consideraciones de arquitectura más complejas, como la inversión de control.
Como mínimo, la mayoría de las clases deberían facilitar la burla de cualquier objeto. Esto puede mejorar significativamente el rendimiento de las pruebas; por ejemplo, probar un método que realice verificaciones de integridad de clave externa (en lugar de confiar en la base de datos para informar errores más adelante) no debería requerir una configuración compleja o la eliminación del escenario de prueba en la base de datos. sí mismo. Además, no debería requerir el método para consultar realmente la base de datos. Estos son todos los éxitos de rendimiento de la prueba y agregan dependencias en una conexión viva y autenticada con la base de datos y, por lo tanto, no pueden manejar otra estación de trabajo que ejecute exactamente la misma prueba al mismo tiempo. En cambio, al simular la conexión de la base de datos, la prueba de la unidad puede configurar fácilmente el escenario en la memoria y pasar el objeto de conexión como una interfaz.
Sin embargo, simplemente burlarse de una clase tampoco es necesariamente la mejor práctica, podría ser mejor refactorizar el código para que toda la información que el método necesita se obtenga por separado, separando la adquisición de los datos del cálculo de los datos. Ahora, el cálculo se puede realizar sin burlarse del objeto responsable de adquirir los datos, lo que simplifica aún más la configuración de la prueba..
Hay un par de estrategias para mitigar los costos que deben considerarse.
La forma más efectiva de reducir el costo de la prueba unitaria es evitar tener que escribir la prueba. Si bien esto suena obvio, ¿cómo se logra esto? La respuesta es asegurarse de que los datos que se pasan al método sean correctos; en otras palabras, entrada correcta, salida correcta (a la inversa de "entrada de basura, salida de basura"). Sí, es probable que aún desee probar el cálculo en sí mismo, pero si puede garantizar que la persona que llama cumple con el contrato, no es necesario probar el método para ver si maneja los parámetros incorrectos (violaciones del contrato).
Esto es un poco de una pendiente resbaladiza porque no tiene idea de cómo podría llamarse el método en el futuro; de hecho, es posible que desee que el método valide su contrato, pero en el contexto en el que se usa actualmente, si puede garantizar que el contrato siempre se cumple, entonces no hay un punto real en escribir pruebas contra el contrato.
¿Cómo se aseguran las entradas correctas? Para los valores que provienen de una interfaz de usuario, filtrar y controlar adecuadamente la interacción del usuario para pre-filtrar los valores es uno de los enfoques. Un enfoque más sofisticado es definir tipos especializados en lugar de confiar en tipos de propósito general. Considere el método de división descrito anteriormente:
public static int Divide (int numerator, int denominator) if (denominator == 0) lanza una nueva ArgumentOutOfRangeException ("Denominator no puede ser 0."); devolver numerador / denominador;
Si el denominador era un tipo especializado que garantizaba un valor distinto de cero:
clase pública NonZeroDouble protected int val; public int Value get return val; establece if (valor == 0) lanza una nueva ArgumentOutOfRangeException ("El valor no puede ser 0"); val = valor;
el método de división nunca tendría que probar este caso:
////// Un ejemplo de uso de especificidad de tipo para evitar una prueba de contrato. /// public static int Divide (int numerator, NonZeroDouble denominator) return numerator / denominator.Value;
Cuando uno considera que esto mejora la especificidad de tipo de la aplicación y establece (con suerte) tipos reutilizables, se da cuenta de cómo esto evita tener que escribir una gran cantidad de pruebas unitarias porque el código a menudo utiliza tipos que son demasiado generales..
Pregúntese: ¿debería mi método ser responsable de manejar las excepciones de terceros, como servicios web, bases de datos, conexiones de red, etc.? Se puede argumentar que la respuesta es "no". Concedido, esto requiere un trabajo inicial adicional: la API de terceros (o incluso el marco) necesita un contenedor que maneje la excepción y una arquitectura en la que el estado interno del la aplicación se puede revertir cuando se produce una excepción y probablemente debería implementarse. Sin embargo, estas mejoras probablemente valgan la pena de todas formas..
Los ejemplos anteriores (entradas correctas, sistemas de tipo especializado, que evitan excepciones de terceros) llevan el problema a un código más general y posiblemente reutilizable. Esto ayuda a evitar escribir la misma validación de contrato o similar, pruebas de unidad de manejo de excepciones, y le permite concentrarse en las pruebas que validan lo que el método debería estar haciendo en condiciones normales, siendo el cálculo en sí mismo..
Como se mencionó anteriormente, existen beneficios de costos definidos para las pruebas unitarias.
Uno de los beneficios obvios es el proceso de formalización de los requisitos del código interno a partir de los requisitos de usabilidad / proceso externos. A medida que se realiza este ejercicio, la dirección con la arquitectura general suele ser un beneficio secundario. Más concretamente, desarrollando un conjunto de pruebas que cumpla con un requisito específico del unidad perspectiva (en lugar de la examen de integración perspectiva) es una prueba objetiva de que el código implementa el requisito.
La prueba de regresión es otro beneficio (a menudo medible). A medida que el código base crece, la verificación de que el código existente aún funciona como estaba previsto ahorra un tiempo de prueba manual considerable y evita el escenario "oops, no probamos eso". Además, cuando se informa de un error, se puede corregir de inmediato, lo que a menudo ahorra a otros miembros del equipo el dolor de cabeza considerable de preguntarse por qué algo en lo que estaban confiando no funciona de repente..
Las pruebas unitarias comprueban no solo que un método se maneja correctamente cuando se le dan entradas incorrectas o excepciones de terceros (como se describió anteriormente, trate de reducir este tipo de pruebas), sino también cómo se espera que el método se comporte en condiciones normales. Esto proporciona una documentación valiosa para los desarrolladores, especialmente los nuevos miembros del equipo: a través de la prueba de la unidad, pueden recopilar fácilmente los requisitos de configuración y los casos de uso. Si su proyecto se somete a una refactorización arquitectónica significativa, las nuevas pruebas unitarias se pueden usar para guiar a los desarrolladores a modificar su código dependiente.
Como se describió anteriormente, una arquitectura más robusta mediante el uso de interfaces, inversión de control, tipos especializados, etc., todo lo cual facilita las pruebas unitarias.-además Mejorar la robustez de la aplicación. Los requisitos cambian, incluso durante el desarrollo, y una arquitectura bien pensada puede manejar esos cambios considerablemente mejor que una aplicación que tiene poca o ninguna consideración arquitectónica.
En lugar de darle a un programador junior un requisito de alto nivel para que se implemente al nivel de habilidad del programador, puede garantizar un mayor nivel de código y éxito (y proporcionar una experiencia de enseñanza) haciendo que el programador junior codifique la implementación contra la prueba en lugar del requisito. Esto elimina una gran cantidad de malas prácticas o conjeturas que un programador junior termina implementando (todos hemos estado allí) y reduce la repetición de tareas que un desarrollador senior debe hacer en el futuro..
Hay varios tipos de revisiones de código. Las pruebas unitarias pueden reducir la cantidad de tiempo empleado en revisar el código por problemas de arquitectura, ya que tienden a imponer la arquitectura. Además, las pruebas unitarias validan el cálculo y también se pueden utilizar para validar todas las rutas de código para un método determinado. Esto hace que las revisiones de códigos sean casi innecesarias: la prueba unitaria se convierte en una autoevaluación del código.
Un efecto secundario interesante de convertir la usabilidad externa o los requisitos del proceso en pruebas de código formalizadas (y su arquitectura de soporte) es que:
Estos descubrimientos, como resultado del proceso de prueba unitaria, identifican problemas al principio del proceso de desarrollo, lo que generalmente ayuda a reducir la confusión, el retrabajo y, por lo tanto, reduce los costos..