Cobertura de código y generación automatizada de casos de prueba JUnit

Por Arthur Hiken

Marzo 16, 2018

6  min leer

Si la cobertura del código es un problema para usted, asegúrese de medirlo correctamente y de medirlo todo a partir de todas las pruebas que ejecuta. Aproveche la generación automática de casos de prueba de cobertura de código JUnit para crear y expandir rápidamente sus pruebas para obtener una cobertura de código completa significativa y mantenible. La cobertura de la prueba unitaria es una excelente manera de asegurarse de que está midiendo todo correctamente.

Profundización en problemas y soluciones de cobertura de código

Hace poco escribí sobre lo fácil que es caer en la trampa de perseguir porcentajes de cobertura de código, lo que llevó a discusiones de calidad, por lo que pensé en profundizar en los problemas y soluciones de cobertura de código. Específicamente, el número de cobertura en sí, el valor de las pruebas JUnit generadas automáticamente y cómo puede identificar las pruebas unitarias que tienen problemas. Y cómo seguir haciendo un mejor trabajo con la ejecución.

¿Cómo funciona la cobertura del código JUnit?

Comencemos con la métrica de cobertura en sí y cómo contamos la cobertura del código. Los números de cobertura de código a menudo no tienen sentido o, en el mejor de los casos, son engañosos. Si "sí" tiene una cobertura de código del 100%, ¿qué significa? ¿Cómo lo mediste?

Hay muchos métodos diferentes para medir la cobertura.

Una forma de medir la cobertura del código es desde la perspectiva de los requisitos. ¿Tiene una prueba para todos y cada uno de los requisitos? Este es un comienzo razonable ... pero no significa que se haya probado todo el código.

Otra forma de medir la cobertura del código (no se ría, de hecho escucho esto en el mundo real) es por el número de pruebas aprobadas. ¡De verdad, lo digo en serio! Esta es una métrica bastante terrible y obviamente sin sentido. ¿Es peor o mejor que simplemente contar cuántas pruebas tiene? No podría decirlo.

Luego llegamos a intentar determinar qué código se ejecutó. Las métricas de cobertura comunes incluyen cobertura de estados de cuenta, cobertura de líneas, cobertura de sucursales, cobertura de decisiones, cobertura de afecciones múltiples o la cobertura más completa MC / DC or Cobertura de decisión / condición modificada.

El método más simple, por supuesto, es la cobertura de línea, pero como probablemente haya visto, las herramientas miden esto de manera diferente, por lo que la cobertura será diferente. Y ejecutar líneas de código no significa que haya verificado todas las cosas diferentes que pueden suceder en esa línea de código. Es por eso que los estándares críticos para la seguridad como ISO 26262 para la seguridad funcional automotriz y DO-178B / C para sistemas aéreos requieren MC / DC.

Aquí hay un ejemplo de código simple, asumiendo que x, yyz son booleanos:

If ((x || y) && z) {hacer algo bueno (); } else {doSomethingElse ();}

En este caso, no importa cuáles sean mis valores, la línea ha sido "cubierta". Es cierto que esta es una forma descuidada de codificar al poner todo en una línea, pero ves el problema. Y la gente escribe código de esta manera. Pero limpiémoslo un poco.

Si ((x || y) && z) {
hacer algo bueno ();
} Else {
hacer algo más(); / * porque el código nunca debería hacer algo malo () * /

Un simple vistazo podría llevarme a la conclusión de que solo necesito dos pruebas, una que evalúe la expresión completa como VERDADERA y se ejecute hacer algo bueno () (x = verdadero, y = verdadero, z = verdadero), y otra prueba que se evalúa como FALSE y se ejecuta doSomethingElse () (x = falso, y = falso, z = falso). La cobertura de línea dice que estamos listos para comenzar, "Todo fue probado".

Pero espere un minuto, hay diferentes formas en que se puede probar la expresión principal:

Valor de xValor de yValor de zValor de la decisión
FalsoFalsoEdición colaborativaFalso
FalsoEdición colaborativaEdición colaborativaEdición colaborativa
FalsoEdición colaborativaFalsoFalso
Edición colaborativaFalsoEdición colaborativaEdición colaborativa

Este es un ejemplo simple, pero ilustra el punto. Necesito 4 pruebas aquí para cubrir realmente el código correctamente, al menos si me importa la cobertura MC / DC. La cobertura de la línea habría dicho 100% cuando estaba a la mitad. Dejaré la explicación más larga sobre el valor de MC / DC para otro momento. El punto aquí es que no importa qué método use para medir la cobertura, es importante que lo que está validando a través de afirmaciones sea significativo.

Cobertura integral del código: cobertura agregada en todas las prácticas de prueba

Autogeneración sin sentido

Otra trampa en la que muchos caen es agregar una herramienta poco sofisticada para generar automáticamente pruebas unitarias.

Las herramientas simples de generación de pruebas crean pruebas que ejecutan código sin ninguna afirmación. Esto evita que las pruebas sean ruidosas, pero todo lo que realmente significa es que su aplicación no se bloquea. Desafortunadamente, esto no le dice si la aplicación está haciendo lo que se supone que debe hacer, lo cual es muy importante.

Imagen de un hombre corriendo escaleras arriba de un billete de un dólar con pliegues que conducen a un gran signo de porcentaje.

La próxima generación de herramientas funciona creando afirmaciones basadas en cualquier valor particular que puedan capturar automáticamente. Sin embargo, si la autogeneración crea un montón de afirmaciones, terminas con un montón de ruido. No hay término medio aquí. O tiene algo que es fácil de mantener pero que no tiene sentido o una pesadilla de mantenimiento que tiene un valor cuestionable.

Muchas herramientas de código abierto que generan automáticamente pruebas unitarias parecen valiosas al principio porque su cobertura aumenta muy rápidamente. Es en el mantenimiento donde ocurren los verdaderos problemas. A menudo, durante el desarrollo, los desarrolladores harán un esfuerzo adicional para ajustar las afirmaciones generadas automáticamente para crear lo que creen que es un conjunto de pruebas limpio. Sin embargo, las afirmaciones son frágiles y no se adaptan a los cambios de código. Esto significa que los desarrolladores deben volver a realizar gran parte de la generación "automática" la próxima vez que lancen. Los conjuntos de pruebas están destinados a ser reutilizados. Si no puede reutilizarlos, está haciendo algo mal.

Esto tampoco cubre la idea más aterradora de que en la primera ejecución, cuando tiene una cobertura alta, las afirmaciones que se encuentran en las pruebas son menos significativas de lo que deberían ser. El hecho de que algo pueda afirmarse no significa que deba serlo o que sea lo correcto.

Public class ListTest {
lista privada list = new ArrayList <> ();
@Prueba
public void testAdd () {
list.add ("Foo");
asertNotNull (lista);
}
}

Idealmente, la aserción es verificar que el código funciona correctamente y la aserción fallará cuando el código no funcione correctamente. Es realmente fácil tener un montón de afirmaciones que no hagan ninguna de las dos cosas, que exploraremos a continuación.

Cobertura bruta vs. Pruebas significativas

Si está buscando un número de alta cobertura a expensas de un conjunto de pruebas sólido, significativo y limpio, pierde valor. Un conjunto de pruebas bien mantenido le da confianza en su código e incluso es la base para una refactorización rápida y segura. Las pruebas ruidosas y / o sin sentido significan que no puede confiar en su suite de pruebas, ni para refactorizar, ni siquiera para su lanzamiento.

Lo que sucede cuando las personas miden su código, especialmente contra estándares estrictos, es que descubren que son más bajos de lo que quieren. Y a menudo esto termina con ellos persiguiendo el número de cobertura. ¡Subamos la cobertura! Y ahora puede entrar en territorio peligroso ya sea por la creencia irrazonable de que las pruebas JUnit automatizadas han creado pruebas significativas o creando pruebas unitarias a mano que tienen poco significado y son costosas de mantener.

En el mundo real, los costos continuos de mantener un conjunto de pruebas superan con creces los costos de crear pruebas unitarias, por lo que es importante que cree buenas pruebas unitarias limpias al principio. Lo sabrá porque podrá ejecutar las pruebas todo el tiempo como parte de su proceso de integración continua (CI). Si solo ejecuta las pruebas en el lanzamiento, es una señal de que las pruebas son más ruidosas de lo que deberían ser. E irónicamente, esto empeora aún más las pruebas porque no se mantienen.

La automatización de las pruebas de software no es mala; de hecho, es necesaria, con la complejidad y las presiones de tiempo que son comunes en la actualidad. Pero la generación automática de valores suele ser más complicada de lo que vale. La automatización basada en la expansión de valores, el monitoreo de sistemas reales y la creación de estructuras complejas, simulacros y stubs proporcionan más valor que la creación sin sentido de afirmaciones.

¿Qué puedes hacer?

Medir

El primer paso es medir y obtener un informe sobre su cobertura actual; de lo contrario, no sabrá dónde se encuentra y si está mejorando. Es importante medir todas las actividades de prueba al hacer esto, incluida la unidad, funcional, manual, etc., y agregar la cobertura correctamente. De esta manera, pondrá su esfuerzo en donde tiene el mayor valor: en el código que no se prueba en absoluto, en lugar del código que está cubierto por sus pruebas de extremo a extremo pero que no tiene una prueba de unidad. Parasoft puede agregar con precisión la cobertura de código de múltiples ejecuciones y múltiples tipos de pruebas para brindarle una medida precisa de dónde se encuentra. Para obtener más información sobre esto, consulte nuestro documento técnico.

Automatice la creación de pruebas de Junit y comience a amar las pruebas unitarias.
Pruebe Parasoft Jtest ahora

Marcos

Las herramientas que crean esqueletos de pruebas unitarias son una buena forma de empezar. Asegúrese de que esas herramientas se conecten a marcos de simulación comunes como Mockito y PowerMock, porque el código real es complicado y requiere apuntar y simular. Pero eso no es suficiente, debe poder:

  • Crea simulacros significativos
  • Amplíe las pruebas sencillas con datos más grandes y más amplios
  • Supervisar una aplicación en ejecución

Asistencia inteligente

Puede hacer todas estas cosas manualmente, pero requiere demasiado tiempo y esfuerzo. Este es un lugar excelente para aprovechar la automatización; por ejemplo, Solución de pruebas unitarias de Parasoft proporciona recomendaciones automáticas en tiempo real en el IDE, integrándose con marcos de código abierto (JUnit, Mockito, PowerMock, etc.) para ayudar al usuario a crear, escalar y mantener su conjunto de pruebas JUnit y brindar una cobertura más amplia. Si tiene curiosidad acerca de esta tecnología, puede obtener más información sobre por qué las personas odian las pruebas unitarias y como recuperar el amor.

Resumen

Si la cobertura es un problema para su proyecto, asegúrese de medirla correctamente y de medir TODAS las pruebas que realice. Y a medida que comienza a expandir su cobertura con pruebas unitarias, puede aprovechar la creación de pruebas guiadas para crear y expandir rápidamente sus pruebas para obtener una cobertura significativa de código mantenible. Prueba J de Parasoft creará pruebas que se pueden mantener a medida que su código crece y cambia, por lo que no está haciendo el mismo trabajo una y otra vez.

Por Arthur Hiken

Arthur ha estado involucrado en seguridad de software y automatización de pruebas en Parasoft durante más de 25 años, ayudando a investigar nuevos métodos y técnicas (incluidas 5 patentes) mientras ayuda a los clientes a mejorar sus prácticas de software.

Reciba las últimas noticias y recursos sobre pruebas de software en su bandeja de entrada.