Me encargaron escribir pruebas unitarias para una aplicación existente. Después de terminar mi primer archivo, tengo 717 líneas de código de prueba para 419 líneas de código original.
¿Esta relación se volverá inmanejable a medida que aumentemos la cobertura de nuestro código?
Mi comprensión de las pruebas unitarias era probar cada método en la clase para asegurar que cada método funcionara como se esperaba. Sin embargo, en la solicitud de extracción, mi líder técnico señaló que debería centrarme en las pruebas de nivel superior. Sugirió probar 4-5 casos de uso que se usan más comúnmente con la clase en cuestión, en lugar de probar exhaustivamente cada función.
Confío en el comentario de mi líder tecnológico. Tiene más experiencia que yo y tiene mejores instintos a la hora de diseñar software. Pero, ¿cómo escribe un equipo de varias personas pruebas para un estándar tan ambiguo? es decir, ¿cómo conozco a mis compañeros y comparto la misma idea para los "casos de uso más comunes"?
Para mí, el 100% de cobertura de prueba unitaria es un objetivo elevado, pero incluso si solo alcanzáramos el 50%, sabríamos que el 100% de ese 50% estaba cubierto. De lo contrario, escribir pruebas para parte de cada archivo deja mucho espacio para hacer trampa.
fuente
Respuestas:
Sí, con una cobertura del 100%, redactará algunas pruebas que no necesita. Desafortunadamente, la única forma confiable de determinar qué pruebas no necesita es escribirlas todas, luego esperar 10 años más o menos para ver cuáles nunca fallaron.
Mantener muchas pruebas no suele ser problemático. Muchos equipos tienen integración automatizada y pruebas del sistema además del 100% de cobertura de prueba unitaria.
Sin embargo, no estás en una fase de mantenimiento de prueba, estás jugando a ponerte al día. Es mucho mejor tener el 100% de sus clases con una cobertura de prueba del 50% que el 50% de sus clases con una cobertura de prueba del 100%, y su líder parece estar tratando de asignarle su tiempo en consecuencia. Después de tener esa línea de base, el siguiente paso generalmente es presionar al 100% en los archivos que se cambian en el futuro.
fuente
Si ha trabajado en grandes bases de código creadas con Test Driven Development, ya sabrá que puede haber demasiadas pruebas unitarias. En algunos casos, la mayor parte del esfuerzo de desarrollo consiste en actualizar las pruebas de baja calidad que se implementarían mejor como comprobaciones invariantes, previas y posteriores a la condición en las clases relevantes, en tiempo de ejecución (es decir, la prueba como un efecto secundario de una prueba de nivel superior )
Otro problema es la creación de diseños de baja calidad, utilizando técnicas de diseño impulsadas por el culto de carga, que resultan en una proliferación de cosas para probar (más clases, interfaces, etc.). En este caso, la carga puede parecer actualizar el código de prueba, pero el verdadero problema es el diseño de baja calidad.
fuente
Respuestas a tus preguntas
Claro ... Podría, por ejemplo, tener múltiples pruebas que parecen ser diferentes a primera vista pero que realmente prueban lo mismo (dependen lógicamente de las mismas líneas de código de aplicación "interesante" bajo prueba).
O podría probar las partes internas de su código que nunca salen a la superficie (es decir, que no forman parte de ningún tipo de contrato de interfaz), donde uno podría discutir si eso tiene sentido. Por ejemplo, la redacción exacta de los mensajes de registro internos o lo que sea.
Eso me parece bastante normal. Sus pruebas emplean muchas líneas de código en la configuración y el desmontaje además de las pruebas reales. La relación puede mejorar o no. Yo mismo tengo muchas pruebas y, a menudo, invierto más loc y tiempo en las pruebas que el código real.
La relación no tiene mucho en cuenta. Hay otras cualidades de las pruebas que tienden a hacerlas inmanejables. Si regularmente tiene que refactorizar una gran cantidad de pruebas cuando realiza cambios bastante simples en su código, debe echar un vistazo a las razones. Y esas no son cuántas líneas tienes, sino cómo te acercas a la codificación de las pruebas.
Eso es correcto para las pruebas de "unidad" en sentido estricto. Aquí, "unidad" es algo así como un método o una clase. El punto de la prueba de "unidad" es probar solo una unidad de código específica, no todo el sistema. Lo ideal sería eliminar todo el resto del sistema (usando dobles o cualquier otra cosa).
Luego caíste en la trampa de asumir que las personas realmente significaban pruebas unitarias cuando decían pruebas unitarias. He conocido a muchos programadores que dicen "prueba unitaria" pero quieren decir algo muy diferente.
Claro, solo concentrarse en el 80% superior del código importante también reduce la carga ... Aprecio que pienses mucho en tu jefe, pero esto no me parece la opción óptima.
No sé qué es la "cobertura de prueba unitaria". Supongo que quiere decir "cobertura de código", es decir, después de ejecutar el conjunto de pruebas, cada línea de código (= 100%) se ha ejecutado al menos una vez.
Esta es una buena métrica de estadio, pero de lejos no es el mejor estándar para el que uno podría disparar. Simplemente ejecutar líneas de código no es la imagen completa; Esto no tiene en cuenta las diferentes rutas a través de ramas complicadas y anidadas, por ejemplo. Es más una métrica que apunta con el dedo a fragmentos de código que se prueban muy poco (obviamente, si una clase tiene una cobertura de código del 10% o 5%, entonces algo está mal); Por otro lado, una cobertura del 100% no le dirá si ha realizado pruebas suficientes o si las ha realizado correctamente.
Pruebas de integración
Me molesta sustancialmente cuando la gente habla constantemente de pruebas unitarias hoy, por defecto. En mi opinión (y experiencia), las pruebas unitarias son excelentes para bibliotecas / API; en áreas más orientadas a los negocios (donde hablamos de casos de uso como en la pregunta en cuestión), no son necesariamente la mejor opción.
Para el código de aplicación general y en el negocio promedio (donde ganar dinero, cumplir con los plazos y cumplir con la satisfacción del cliente es importante, y principalmente desea evitar errores que están directamente en la cara del usuario o que podrían conducir a desastres reales , no estamos hablando de los lanzamientos de cohetes de la NASA aquí), las pruebas de integración o características son mucho más útiles.
Esos van de la mano con el desarrollo dirigido por el comportamiento o el desarrollo basado en características; aquellos que no funcionan con pruebas unitarias (estrictas), por definición.
Para mantenerlo breve (ish), una prueba de integración / función ejercita toda la pila de aplicaciones. En una aplicación basada en la web, actuaría como un navegador que hace clic en la aplicación (y no, obviamente, no tiene que ser tan simplista, existen marcos muy potentes para hacer eso; consulte http: // pepino. io por ejemplo).
Ah, para responder a sus últimas preguntas: hace que todo su equipo tenga una cobertura de prueba alta asegurándose de que una nueva función solo se programe después de que su prueba de función se haya implementado y haya fallado. Y sí, eso significa cada característica. Esto le garantiza una cobertura de características del 100% (positiva). Por definición, garantiza que una característica de su aplicación nunca "desaparecerá". No garantiza una cobertura de código del 100% (por ejemplo, a menos que programe activamente funciones negativas, no ejercerá su manejo de errores / manejo de excepciones).
No le garantiza una aplicación libre de errores; por supuesto, querrá escribir pruebas de características para situaciones de errores obvias o muy peligrosas, entradas incorrectas del usuario, piratería (por ejemplo, administración de sesiones, seguridad y demás), etc .; pero incluso solo programar las pruebas positivas tiene un beneficio enorme y es bastante factible con marcos modernos y potentes.
Las pruebas de características / integración obviamente tienen su propia lata de gusanos (p. Ej., Rendimiento; pruebas redundantes de marcos de terceros; como generalmente no usas dobles, también tienden a ser más difíciles de escribir, según mi experiencia ...), pero yo ' d tome una aplicación 100% de prueba de características positivas sobre una aplicación 100% de prueba de unidad de cobertura de código (¡no biblioteca!) cualquier día.
fuente
Sí, es posible tener demasiadas pruebas unitarias. Si tiene una cobertura del 100% con pruebas unitarias y sin pruebas de integración, por ejemplo, tiene un problema claro.
Algunos escenarios:
Sobre-diseñas tus pruebas para una implementación específica. Luego, debe descartar las pruebas unitarias cuando refactoriza, por no decir cuándo cambia la implementación (un punto muy frecuente cuando se realizan optimizaciones de rendimiento).
Un buen equilibrio entre las pruebas unitarias y las pruebas de integración reducen este problema sin perder una cobertura significativa.
Podrías tener una cobertura razonable para cada confirmación con el 20% de las pruebas que tienes, dejando el 80% restante para la integración o al menos pasar las pruebas por separado; Los principales efectos negativos que ve en este escenario son cambios lentos, ya que debe esperar mucho tiempo para que se ejecuten las pruebas.
Modifica demasiado el código para permitirle probarlo; Por ejemplo, he visto mucho abuso de IoC en componentes que nunca requerirán ser modificados o al menos es costoso y de baja prioridad generalizarlos, pero la gente invierte mucho tiempo en generalizarlos y refactorizarlos para permitir que la unidad los pruebe. .
Estoy particularmente de acuerdo con la sugerencia de obtener una cobertura del 50% en el 100% de los archivos, en lugar de una cobertura del 100% en el 50% de los archivos; centre sus esfuerzos iniciales en los casos positivos más comunes y en los casos negativos más peligrosos, no invierta demasiado en el manejo de errores y caminos inusuales, no porque no sean importantes sino porque tiene un tiempo limitado y un universo de pruebas infinito, así que debes priorizar en cualquier caso.
fuente
Tenga en cuenta que cada prueba tiene un costo y un beneficio. Los inconvenientes incluyen:
Si los costos son mayores que los beneficios, es mejor no realizar una prueba. Por ejemplo, si la funcionalidad es difícil de probar, la API cambia a menudo, la corrección es relativamente poco importante y la posibilidad de que la prueba encuentre un defecto es baja, probablemente sea mejor no escribirla.
En cuanto a su proporción particular de pruebas a código, si el código es lo suficientemente denso lógico, entonces esa proporción puede justificarse. Sin embargo, probablemente no valga la pena mantener una relación tan alta en una aplicación típica.
fuente
Sí, existen demasiadas pruebas unitarias.
Si bien las pruebas son buenas, cada prueba unitaria es:
Una carga de mantenimiento potencial que está estrechamente vinculada a la API
Tiempo que podría gastarse en otra cosa.
Es aconsejable apuntar a una cobertura de código del 100%, pero eso dista mucho de significar un conjunto de pruebas, cada una de las cuales proporciona de forma independiente una cobertura de código del 100% en algún punto de entrada específico (función / método / llamada, etc.).
Aunque dado lo difícil que puede ser lograr una buena cobertura y eliminar errores, la verdad es que probablemente existan "pruebas unitarias incorrectas" y "demasiadas pruebas unitarias".
La pragmática para la mayoría del código indica:
Asegúrese de tener una cobertura del 100% de los puntos de entrada (todo se prueba de alguna manera) y procure estar cerca del 100% de cobertura del código de las rutas 'sin errores'.
Pruebe cualquier valor o tamaño mínimo / máximo relevante
Pruebe cualquier cosa que piense que es un caso especial divertido, particularmente valores "extraños".
Cuando encuentre un error, agregue una prueba de unidad que hubiera revelado ese error y piense si se deben agregar casos similares.
Para algoritmos más complejos, considere también:
Por ejemplo, verifique un algoritmo de clasificación con alguna entrada aleatoria y la validación de los datos se clasifica al final escaneándola.
Yo diría que su líder tecnológico está proponiendo pruebas de 'mínimo culo desnudo'. Estoy ofreciendo 'pruebas de calidad de mayor valor' y hay un espectro intermedio.
Tal vez su superior sabe que el componente que está construyendo se incrustará en una pieza más grande y la unidad se probará más a fondo cuando se integre.
La lección clave es agregar pruebas cuando se encuentran errores. Lo que me lleva a mi mejor lección sobre el desarrollo de pruebas unitarias:
Concéntrese en unidades, no en subunidades. Si está construyendo una unidad a partir de subunidades, escriba pruebas muy básicas para las subunidades hasta que sean plausibles y logre una mejor cobertura probando las subunidades a través de sus unidades de control.
Entonces, si está escribiendo un compilador y necesita escribir una tabla de símbolos (por ejemplo). Obtenga la tabla de símbolos en funcionamiento con una prueba básica y luego trabaje (digamos) el analizador de declaraciones que llena la tabla. Solo agregue más pruebas a la unidad 'independiente' de la tabla de símbolos si encuentra errores en ella. De lo contrario, aumente la cobertura mediante pruebas unitarias en el analizador de declaraciones y luego en todo el compilador.
Eso obtiene el mejor rendimiento (una prueba del conjunto es probar múltiples componentes) y deja más capacidad para rediseñar y refinar porque solo se usa la interfaz 'externa' en las pruebas que tienden a ser más estables.
Junto con las condiciones previas y posteriores de las pruebas de código de depuración, incluidas las invariantes en todos los niveles, obtiene la máxima cobertura de prueba desde una implementación de prueba mínima.
fuente
En primer lugar, no es necesariamente un problema tener más líneas de prueba que de código de producción. El código de prueba es (o debería ser) lineal y fácil de comprender: su complejidad necesaria es muy, muy baja, independientemente de si el código de producción es o no. Si la complejidad de las pruebas comienza a acercarse a la del código de producción, entonces es probable que tenga un problema.
Sí, es posible tener demasiadas pruebas unitarias; un simple experimento mental muestra que puede continuar agregando pruebas que no proporcionan un valor adicional, y que todas esas pruebas agregadas pueden inhibir al menos algunas refactorizaciones.
El consejo de probar solo los casos más comunes es defectuoso, en mi opinión. Estos pueden actuar como pruebas de humo para ahorrar tiempo en las pruebas del sistema, pero las pruebas realmente valiosas detectan casos que son difíciles de ejercer en todo el sistema. Por ejemplo, la inyección de error controlada de fallas de asignación de memoria se puede utilizar para ejercer rutas de recuperación que de otro modo podrían ser de calidad completamente desconocida. O pase cero como un valor que sabe que se usará como divisor (o un número negativo que tendrá raíz cuadrada), y asegúrese de no obtener una excepción no controlada.
Las siguientes pruebas más valiosas son aquellas que ejercen los límites extremos o puntos límite. Por ejemplo, una función que acepta meses (basados en 1) del año debe probarse con 0, 1, 12 y 13, para que sepa que las transiciones válidas no válidas están en el lugar correcto. Es una prueba excesiva usar también 2..11 para estas pruebas.
Estás en una posición difícil, ya que tienes que escribir pruebas para el código existente. Es más fácil identificar los casos extremos mientras escribe (o está a punto de escribir) el código.
fuente
Esta comprensión está mal.
Las pruebas unitarias verifican el comportamiento de la unidad bajo prueba .
En ese sentido, una unidad no es necesariamente "un método en una clase". Me gusta la definición de una unidad de Roy Osherove en The Art of Unit Testing :
En base a esto, una prueba unitaria debería verificar cada comportamiento deseado de su código. Donde el "deseo" se toma más o menos de los requisitos.
Tiene razón, pero de una manera diferente de lo que piensa.
Por su pregunta, entiendo que usted es el "probador dedicado" en ese proyecto.
El gran malentendido es que él espera que escribas pruebas unitarias (en contraste con "prueba usando un marco de pruebas unitarias"). Escribir y probar pruebas es responsabilidad de los desarrolladores , no de los evaluadores (en un mundo ideal, lo sé ...). Por otro lado, etiquetó esta pregunta con TDD, lo que implica exactamente esto.
Su trabajo como probador es escribir (o ejecutar manualmente) pruebas de módulo y / o aplicación. Y este tipo de pruebas debería verificar principalmente que todas las unidades funcionen juntas sin problemas. Eso significa que debe seleccionar sus casos de prueba para que cada unidad se ejecute al menos una vez . Y esa verificación es que se ejecuta. El resultado real es menos importante ya que está sujeto a cambios con los requisitos futuros.
Para enfatizar la analogía del volcado de automóviles una vez más: ¿Cuántas pruebas se realizan con un automóvil al final de la línea de montaje? Exactamente uno: debe conducir al estacionamiento por sí mismo ...
El punto aquí es:
Debemos ser conscientes de esa diferencia entre "pruebas unitarias" y "prueba automatizada utilizando un marco de pruebas unitarias".
Las pruebas unitarias son una red de seguridad. Le dan confianza para refactorizar su código para reducir la deuda técnica o agregar un nuevo comportamiento sin temor a romper el comportamiento ya implementado.
No necesita una cobertura del código del 100%.
Pero necesita una cobertura de comportamiento del 100%. (Sí, la cobertura de código y la cobertura de comportamiento se correlacionan de alguna manera, pero no son idénticas por el simple hecho de hacerlo).
Si tiene menos del 100% de cobertura de comportamiento, una ejecución exitosa de su conjunto de pruebas no significa nada, ya que podría haber cambiado parte del comportamiento no probado. Y su cliente lo notará el día después de que su lanzamiento se haya conectado ...
Conclusión
Pocas pruebas son mejores que ninguna prueba. ¡Sin duda!
Pero no existe tal cosa como tener demasiadas pruebas unitarias.
Esto se debe a que cada prueba unitaria verifica una sola expectativa sobre el comportamiento de los códigos . Y no puede escribir más pruebas unitarias de las que espera de su código. Y un agujero en su arnés de seguridad es una oportunidad para que un cambio no deseado dañe el sistema de producción.
fuente
Absolutamente sí. Solía ser un SDET para una gran empresa de software. Nuestro pequeño equipo tuvo que mantener el código de prueba que solía manejar un equipo mucho más grande. Además de eso, nuestro producto tenía algunas dependencias que constantemente introducían cambios importantes, lo que significaba un mantenimiento de prueba constante para nosotros. No teníamos la opción de aumentar el tamaño del equipo, por lo que tuvimos que tirar miles de las pruebas menos valiosas cuando fallaron. De lo contrario, nunca podríamos estar al día con los defectos.
Antes de descartar esto como un mero problema de gestión, tenga en cuenta que muchos proyectos en el mundo real sufren una reducción de personal a medida que se acercan al estado heredado. A veces, incluso comienza a suceder justo después del primer lanzamiento.
fuente
Tener más líneas de código de prueba que código de producto no es necesariamente un problema, suponiendo que esté refactorizando su código de prueba para eliminar copiar y pegar.
Lo que es un problema es tener pruebas que sean espejos de su implementación, sin ningún significado comercial, por ejemplo, pruebas cargadas con simulacros y apéndices y solo afirmando que un método llama a otro método.
Una gran cita en el documento "por qué la mayoría de las pruebas unitarias son desperdicios" es que las pruebas unitarias deben tener un "oráculo de corrección amplio, formal e independiente, y ... valor comercial atribuible"
fuente
Una cosa que no vi mencionado es que sus pruebas deben ser rápidas y fáciles para que cualquier desarrollador las ejecute en cualquier momento.
No desea tener que registrarse en el control de origen y esperar una hora o más (dependiendo del tamaño de su base de código) antes de que se completen las pruebas para ver si su cambio rompió algo; desea poder hacerlo en su propia máquina antes de registrarse en el control de origen (o al menos, antes de impulsar sus cambios). Idealmente, debería poder ejecutar sus pruebas con un solo script o presionando un botón.
Y cuando ejecuta esas pruebas localmente, desea que se ejecuten rápidamente, en el orden de segundos. Un poco más lento, y tendrás la tentación de no ejecutarlos lo suficiente o en absoluto.
Por lo tanto, tener tantas pruebas que ejecutarlas toma minutos, o tener algunas pruebas demasiado complejas, podría ser un problema.
fuente