Pruebas unitarias sucintamente mire antes de saltar

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..

Código de prueba de unidad frente al código que se está probando

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:

  • Planificación
  • Desarrollo
  • Pruebas (sí, pruebas unitarias deben ser probadas)

Además, las pruebas unitarias también pueden:

  • Tener un código base más grande que el código de producción..
  • Necesita ser sincronizado cuando cambia el código de producción.
  • Tienden a imponer direcciones arquitectónicas y patrones de implementación..

La base del código de prueba de unidad puede ser mayor que el código de producción

Al determinar si las pruebas se pueden escribir con un solo método, se debe considerar:

  • ¿Valida el contrato??
  • ¿Funciona correctamente el cálculo??
  • Es el estado interno del objeto establecido correctamente?
  • ¿Devuelve el objeto a un estado "sano" si se produce una excepción??
  • ¿Están todas las rutas de código probadas??
  • ¿Qué requisitos de configuración o desmontaje tiene el método??

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..


Mantenimiento de pruebas unitarias

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:

  • Nuevo código o cambios al código existente que mejoran la experiencia del usuario.
  • Reestructuración significativa para cumplir con los requisitos que la arquitectura existente no admite..

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:

  • Refactorización de parámetros de clases concretas a interfaces o clases abstractas..
  • Refactorizando la jerarquía de clases..
  • Sustitución de una tecnología de terceros por otra.
  • Refactorizar el código para que sea asíncrono o tareas de soporte..
  • Otros:
    • Ejemplo: cambiar de una clase de base de datos concreta, como SqlConnection a IDbConnection, de modo que el código admita diferentes bases de datos y requiera volver a trabajar las pruebas unitarias que llaman a métodos que dependían de clases concretas para sus parámetros.
    • Ejemplo: modificar un modelo para utilizar un formato de serialización estándar, como XML, en lugar de una metodología de serialización personalizada.
    • Ejemplo: el cambio de un ORM interno a un ORM de terceros, como Entity Framework, puede requerir cambios considerables en la configuración o el desmontaje de las pruebas unitarias..

La prueba unitaria hace cumplir un paradigma de arquitectura?

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.


Rendimiento de la prueba unitaria

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..


Costes atenuantes

Hay un par de estrategias para mitigar los costos que deben considerarse.

Entradas correctas

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..

Evitar excepciones de terceros

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..

Evite escribir las mismas pruebas para cada método

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..


Beneficios de costos

Como se mencionó anteriormente, existen beneficios de costos definidos para las pruebas unitarias.

Codificación para el requisito

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.

Reduce los errores en sentido descendente

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..

Los casos de prueba proporcionan una forma de documentación

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.

Hacer cumplir un paradigma de arquitectura mejora la arquitectura

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.

Programadores Junior

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..

Revisiones de Código

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.

Convirtiendo requisitos en pruebas

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:

  • A menudo se descubren problemas con los requisitos..
  • Requerimientos arquitectónicos son traídos a la luz..
  • Se identifican supuestos y otras lagunas en los requisitos..

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..