unit-testing - unitarias - unit test coverage visual studio
Cobertura del código de prueba unitaria: ¿tiene cobertura del 100%? (15)
Del blog de Ted Neward .
En este punto en el tiempo, la mayoría de los desarrolladores al menos han oído hablar, si no se considera la adopción de, el meme de prueba masoquista. Compañeros de NFJS Stuart Halloway y Justin Gehtland han fundado una empresa de consultoría, Relevance, que establece un estándar alto como estándar cultural corporativo: cobertura 100% de prueba de su código.
Neal Ford ha informado que ThoughtWorks hace declaraciones similares, aunque entiendo que los clientes a veces ponen obstáculos accidentales en su camino para lograr dicho objetivo. Es ambicioso, pero como dice el antiguo proverbio indio americano,
Si apuntas tu flecha al sol, volará más alto y más lejos que si lo apuntas al suelo.
¿Sus pruebas unitarias constituyen una cobertura de código del 100%? Sí o no, y por qué o por qué no.
En muchos casos, no vale la pena obtener una cobertura del estado del 100%, pero en algunos casos, vale la pena. En algunos casos, la cobertura del estado del 100% es un requisito demasiado laxo .
La pregunta clave es: "¿cuál es el impacto si el software falla (produce el resultado incorrecto)?". En la mayoría de los casos, el impacto de un error es relativamente bajo. Por ejemplo, tal vez tenga que arreglar el código dentro de unos días y volver a ejecutar algo. Sin embargo, si el impacto es "alguien puede morir en 120 segundos", entonces ese es un gran impacto, y debería tener mucha más cobertura de prueba que solo el 100% de cobertura de extracto.
Dirijo la Insignia de Mejores Prácticas de la Iniciativa de Infraestructura Central para la Fundación Linux. Tenemos una cobertura del estado del 100%, pero no diría que fue estrictamente necesario. Durante mucho tiempo estuvimos muy cerca del 100%, y decidimos hacer ese último porcentaje. Sin embargo, no pudimos justificar el último por ciento en terrenos de ingeniería; esos últimos por ciento fueron agregados puramente como "orgullo de la mano de obra". Me da un poco de tranquilidad tener una cobertura del 100%, pero realmente no era necesario. Teníamos más del 90% de cobertura de estado solo por las pruebas normales, y eso estuvo bien para nuestros propósitos. Dicho esto, queremos que el software sea sólido como una roca, y tener una cobertura de estado del 100% nos ha ayudado a llegar allí. También es más fácil obtener una cobertura del estado del 100% hoy.
Todavía es útil medir la cobertura, incluso si no necesita el 100%. Si sus pruebas no tienen una cobertura decente, debería preocuparse. Un conjunto de pruebas incorrecto puede tener una buena cobertura de declaración, pero si no tiene una buena cobertura de declaración, entonces, por definición, tiene un conjunto de pruebas incorrecto. Lo que necesita es una compensación: ¿cuáles son los riesgos (probabilidad e impacto) del software que no se han probado? Por definición, es más probable que tenga errores (¡no lo probó!), Pero si usted y sus usuarios pueden vivir con esos riesgos (probabilidad e impacto), está bien. Para muchos proyectos de menor impacto, creo que la cobertura del estado de cuenta del 80% -90% está bien, con mejor ser mejor.
Por otro lado, si las personas pueden morir a causa de errores en su software, la cobertura del estado del 100% no es suficiente. Al menos agregaría una cobertura de sucursal, y tal vez más, para verificar la calidad de sus pruebas. Los estándares como DO-178C (para sistemas aerotransportados) adoptan este enfoque: si una falla es menor, no es gran cosa, pero si una falla puede ser catastrófica, entonces se requieren pruebas mucho más rigurosas. Por ejemplo, DO-178C requiere cobertura MC / DC para el software más crítico (el software que puede matar rápidamente a las personas si comete un error). MC / DC es mucho más extenuante que la cobertura de extracto o incluso la cobertura de sucursal.
En un nuevo proyecto, practico TDD y mantengo una cobertura de línea del 100%. En su mayoría ocurre de forma natural a través de TDD. Las lagunas de cobertura generalmente merecen la atención y se satisfacen fácilmente. Si la herramienta de cobertura que estoy usando brinda cobertura de sucursales u otra cosa, le prestaré atención, aunque nunca he visto la cobertura de sucursales decirme nada, probablemente porque TDD llegó primero.
Mi argumento más sólido para mantener una cobertura del 100% (si le importa la cobertura) es que es mucho más fácil mantener una cobertura del 100% que administrar menos del 100% . Si tiene una cobertura del 100% y se cae, inmediatamente sabe por qué y puede solucionarlo fácilmente, porque la caída está en el código en el que acaba de trabajar. Pero si se conforma con el 95% o lo que sea, es fácil pasar por alto las regresiones de cobertura y siempre volverá a revisar los vacíos conocidos. Es la razón exacta por la cual las mejores prácticas actuales requieren que el conjunto de pruebas de uno pase por completo. Cualquier cosa menos es más difícil, no más fácil de manejar.
Mi actitud definitivamente se ve reforzada por haber trabajado en Ruby durante algún tiempo, donde existen excelentes marcos de prueba y los exámenes dobles son fáciles. La cobertura del 100% también es fácil en Python. Puede que tenga que reducir mis estándares en un entorno con herramientas menos susceptibles.
Me encantaría tener los mismos estándares en proyectos heredados, pero nunca me pareció práctico llevar una gran aplicación con cobertura mediocre hasta el 100% de cobertura; Tuve que conformarme con el 95-99%. Siempre ha sido demasiado trabajo para volver atrás y cubrir todo el código anterior. Esto no contradice mi argumento de que es fácil mantener una base de código al 100%; es mucho más fácil cuando mantienes ese estándar desde el principio.
Generalmente escribo pruebas unitarias solo como un método de prevención de regresión. Cuando se informa que tengo un error que corregir, creo una prueba de unidad para asegurarme de que no vuelva a aparecer en el futuro. Puedo crear algunas pruebas para las secciones de funcionalidad que tengo para asegurarme de que permanezca intacto (o para interacciones complejas entre las partes), pero generalmente quiero que la corrección de errores me diga que es necesario.
Lo que hago cuando tengo la oportunidad es insertar declaraciones en cada rama del código que se puede analizar y registrar si han sido afectadas, de modo que pueda hacer algún tipo de comparación para ver qué declaraciones no se han visto afectadas. . Esto es un poco pesado, así que no siempre soy bueno al respecto.
Acabo de construir una pequeña aplicación de interfaz de usuario para usar en subastas de caridad, que usa MySQL como su base de datos. Como realmente no quería romper en medio de una subasta, intenté algo nuevo.
Como estaba en VC6 (C ++ + MFC), definí dos macros:
#define TCOV ASSERT(FALSE)
#define _COV ASSERT(TRUE)
y luego rocié
TCOV;
a lo largo del código, en cada camino separado que pude encontrar, y en cada rutina. Luego ejecuté el programa bajo el depurador, y cada vez que TCOV
un TCOV
, se detenía. Me gustaría ver el código de cualquier problema obvio, y luego editarlo a _COV
, luego continuar. El código se recompilaría sobre la marcha y pasaría al siguiente TCOV
. De esta forma, lentamente, laboriosamente, TCOV
suficientes declaraciones de TCOV
para que funcionara "normalmente".
Después de un tiempo, agrupé el código para TCOV
, y eso mostró qué código no había probado. Luego volví y lo volví a ejecutar, asegurándome de probar más ramas que no había probado antes. Seguí haciendo esto hasta que no quedaron declaraciones TCOV
en el código.
Esto tomó algunas horas, pero en el proceso encontré y solucioné varios errores. No hay manera de que pudiera tener la disciplina para hacer y seguir un plan de prueba que hubiera sido tan exhaustivo. No solo sabía que había cubierto todas las ramas, sino que me había hecho ver cada rama mientras estaba funcionando, un muy buen tipo de revisión de código.
Por lo tanto, ya sea que use o no una herramienta de cobertura, esta es una buena manera de eliminar los errores que de lo contrario acecharían en el código hasta un momento más embarazoso.
No por varias razones:
- Es realmente costoso alcanzar la cobertura del 100%, en comparación con el 90% o el 95% de un beneficio que no es obvio.
- Incluso con el 100% de cobertura, su código no es perfecto. Eche un vistazo a este método (de hecho, depende del tipo de cobertura de la que está hablando: cobertura de sucursal , cobertura de línea ...):
public static String foo(boolean someCondition) {
String bar = null;
if (someCondition) {
bar = "blabla";
}
return bar.trim();
}
y la prueba de la unidad:
assertEquals("blabla", foo(true));
La prueba tendrá éxito y la cobertura de tu código es del 100%. Sin embargo, si agrega otra prueba:
assertEquals("blabla", foo(false));
entonces obtendrás una NullPointerException
. ¡Y como estuvo al 100% con la primera prueba, no necesariamente habría escrito la segunda!
En general, considero que el código crítico debe cubrirse casi al 100%, mientras que el otro código puede cubrirse al 85-90%
No, porque hay una compensación práctica entre pruebas de unidades perfectas y realmente terminar un proyecto :)
No, porque me dediqué a agregar nuevas funciones que ayudan a los usuarios en lugar de complicar la tarea de escribir pruebas oscuras que ofrecen poco valor. Yo digo prueba unitaria las cosas grandes, las cosas sutiles y las cosas que son frágiles.
Para todos los probadores de cobertura del 90%:
El problema al hacerlo es que el 10% de código difícil de probar es también el código no trivial que contiene el 90% del error. Esta es la conclusión que obtuve empíricamente después de muchos años de TDD.
Y después de todo, esta es una conclusión bastante directa. Este 10% de código difícil de probar, es difícil de probar porque refleja un problema empresarial difícil o un error de diseño complicado o ambos. Estas razones exactas que a menudo conducen a un código defectuoso.
Pero también:
- El código 100% cubierto que disminuye con el tiempo a menos del 100% cubierto a menudo identifica un error o al menos un defecto.
- El código 100% cubierto que se usa junto con los contratos es la mejor arma para llevar a vivir cerca del código libre de errores. Los contratos de código y las pruebas automatizadas son más o menos lo mismo
- Cuando se descubre un error en un código cubierto al 100%, es más fácil de arreglar. Dado que el código responsable del error ya está cubierto por las pruebas, no debería ser difícil escribir nuevas pruebas para cubrir la corrección de errores.
Personalmente encuentro que la cobertura del 100% de prueba es problemática en múltiples niveles. En primer lugar, debe asegurarse de obtener un beneficio tangible y económico de las pruebas de unidad que redacta. Además, las pruebas unitarias, como cualquier otro código, son CÓDIGO. Eso significa que, al igual que cualquier otro código, debe verificarse para su corrección y mantenerse. Ese tiempo adicional para verificar la corrección del código adicional, y mantenerlo y mantener esas pruebas válidas en respuesta a los cambios en el código comercial, agrega un costo. Lograr una cobertura del 100% de las pruebas y asegurarse de que prueba su código lo más exhaustivamente posible es un esfuerzo encomiable, pero lograrlo a cualquier costo ... bueno, a menudo es demasiado costoso.
Muchas veces, cuando se cubren los errores y las verificaciones de validez para cubrir casos marginales o extremadamente raros, pero definitivamente posibles, los casos excepcionales son un ejemplo de código que no necesariamente debe cubrirse. La cantidad de tiempo , esfuerzo (y, en última instancia, dinero ) que debe invertirse para lograr la cobertura de tales casos raros de margen suele ser un desperdicio a la luz de otras necesidades comerciales. Las propiedades son a menudo parte del código que, especialmente con C # 3.0, no necesitan ser probadas ya que la mayoría de las propiedades, si no todas, se comportan exactamente de la misma manera y son excesivamente simples (retorno o conjunto de declaración única). Invertir cantidades enormes de las pruebas de unidad de envoltura de tiempo alrededor de miles de propiedades bien podría invertirse mejor en otro lugar donde se pueda obtener un retorno tangible mayor y más valioso de esa inversión.
Más allá de simplemente lograr una cobertura de prueba del 100%, existen problemas similares al tratar de configurar la unidad "perfecta". Los marcos burlones han progresado a un grado sorprendente en estos días, y casi cualquier cosa puede ser burlada (si estás dispuesto a pagar dinero, TypeMock puede burlarse de cualquier cosa y todo, pero cuesta mucho). Sin embargo, a menudo hay momentos en que las dependencias de su código no se escribieron de manera simulada (esto es en realidad un problema central con la gran mayoría del framework .NET). Invertir tiempo para lograr el alcance adecuado de una prueba es útil, pero poner cantidades excesivas de Es hora de burlarse de todo y de cualquier cosa bajo la luz del sol, al agregar capas de abstracción e interfaces para hacerlo posible, nuevamente es más una pérdida de tiempo, esfuerzo y finalmente dinero.
El objetivo final con las pruebas no debería ser lograr la máxima cobertura de código. El objetivo final debe ser lograr el mayor valor por unidad de tiempo invertido en escribir pruebas unitarias, mientras cubre tanto como sea posible en ese momento. La mejor manera de lograr esto es tomar el enfoque BDD: especifique sus inquietudes, defina su contexto y verifique los resultados esperados de cualquier comportamiento que se desarrolle (comportamiento ... no unidad).
Por lo general, logro llegar al 93 ... 100% con mi cobertura, pero ya no aspiro al 100%. Solía hacer eso y aunque es factible, no vale la pena el esfuerzo más allá de un cierto punto, porque las pruebas ciegamente obvias generalmente no son necesarias. Un buen ejemplo de esto podría ser la rama de evaluación verdadera del siguiente código cortado
public void method(boolean someBoolean) {
if (someBoolean) {
return;
} else {
/* do lots of stuff */
}
}
Sin embargo, lo que es importante lograr es una cobertura lo más cercana posible al 100% de las partes funcionales de la clase, ya que esas son las aguas peligrosas de tu aplicación, el pantano brumoso de insectos rastreros y el comportamiento indefinido y, por supuesto, el circo de las pulgas.
Rara vez es práctico obtener una cobertura de código del 100% en un sistema no trivial. La mayoría de los desarrolladores que escriben pruebas unitarias disparan entre mediados y finales de los 90.
Una herramienta de prueba automatizada como Pex puede ayudar a aumentar la cobertura del código. Funciona mediante la búsqueda de casos extremos difíciles de encontrar.
Sí, he tenido proyectos que han tenido una cobertura de línea del 100%. Vea mi respuesta a una question. similar question.
Puede obtener una cobertura de línea del 100%, pero como otros han señalado aquí en SO y en otros sitios en Internet, tal vez sea solo un mínimo. Cuando considera la cobertura de rutas y sucursales, hay mucho más trabajo por hacer.
La otra forma de verlo es tratar de hacer que su código sea tan simple que sea fácil obtener una cobertura de línea del 100%.
Sí.
Sin embargo, depende de qué idioma y marco utilices para qué tan fácil es lograrlo.
Estamos usando Ruby on Rails para mi proyecto actual. Ruby es muy "simulable" en el hecho de que puedes resguardar o burlarse de grandes porciones de tu código sin tener que construir diseños de construcción y composición de clases demasiado complicados que tendrías que hacer en otros idiomas.
Dicho esto, solo tenemos una cobertura de línea del 100% (básicamente, lo que le proporciona el rcov). Aún debes pensar en probar todas las ramas requeridas.
Esto solo es posible si lo incluye desde el principio como parte de su compilación de integración continua y rompe la compilación si la cobertura cae por debajo del 100%, lo que hace que los desarrolladores lo solucionen de inmediato. Por supuesto, podría elegir otro número como objetivo, pero si está comenzando de cero, no hay mucha diferencia para el esfuerzo de llegar del 90% al 100%
También tenemos un montón de otras métricas que rompen la construcción si también cruzan un umbral dado (complejidad ciclomática, duplicación, por ejemplo). Todas van juntas y ayudan a reforzarse mutuamente.
Una vez más, debes tener estas cosas en su lugar desde el principio para seguir trabajando en un nivel estricto, ya sea eso o establecer un objetivo al que puedas golpear, y gradualmente aumentarlo hasta que llegues al nivel que te hace feliz.
¿Hacer esto agrega valor? Al principio era escéptico, pero honestamente puedo decir que sí. No principalmente porque ha probado el código (aunque definitivamente es un beneficio), sino más bien en términos de escribir código simple que es fácil de probar y razonar. Si sabes que tienes que tener una cobertura de prueba del 100%, dejas de escribir demasiado complejo si / else / while / try / catch monstrosities y Keep It Simple Stupid.
Solo tengo una cobertura del 100% de las nuevas piezas de código que se han escrito teniendo en cuenta la capacidad de prueba . Con la encapsulación adecuada, cada clase y función puede tener pruebas de unidades funcionales que brinden simultáneamente una cobertura cercana al 100%. Es solo cuestión de agregar algunas pruebas adicionales que cubren algunos casos extremos para que llegue al 100%.
No debe escribir pruebas solo para obtener cobertura. Debería escribir pruebas funcionales que prueben la corrección / cumplimiento. Con una buena especificación funcional que cubre todos los motivos y un buen diseño de software, puede obtener una buena cobertura de forma gratuita.