miércoles, 26 de agosto de 2009

El % cobertura no significa nada

Los que venimos del testing, que raro nos suena escuchar a programadores hablar sobre la calidad del software. ¿No es ese nuestro terreno?

La llegada del desarrollo Ágil a creado muchas mejoras en la forma de trabajo. El énfasis en la calidad ha logrado que muchos programadores incorporen la idea de testing como algo necesario en el proceso de desarrollo.

Los test unitarios, hace no mucho tiempo un lujo poco frecuente, se han ido incorporando como una buena práctica en muchos equipos. Esto, junto con otras prácticas como integración continua, pair programming e involucramiento del cliente durante toda la duración del proyecto han requerido un cambio de las actividades realizadas por los miembros del equipo (como ejemplo, ver mi visión sobre los cambios de los testers en Tester's Dilemma, para el caso de los analistas, puede verse la presentación de Cooper).

Pero los cambios también trajeron algunos nuevos problemas. En los últimos tiempos he escuchado discusiones entre personas a las que respeto mucho sobre si es posible o no llegar al 100% de cobertura. En una reciente discusión en foro-agiles, se ha propuesto un umbral de % de cobertura como una requerimiento contractual para equipos externos (outsourcing).

Por un lado... ¡música para mis oídos! Prefiero mil veces discusciones sobre cuánto debemos probar y cómo (automatizado vs exploratorio, unitario vs integración vs sistema, …), quién (testers, programadores) antes que discutir sobre si tenemos o no que probar.

Por otro lado...

¡El porcentaje de cobertura no significa nada!

Bien, ya tiré la bomba... ahora a tratar de justificarlo.

Desmenucemos un poco el problema. Cuando un programador habla de cobertura, en general se refiere a cobertura de lineas de código por pruebas unitarias automáticas. Empiezo entonces por mostrar que la cobertura, en ese sentido, puede no significar mucho. Luego vamos por los otros significados que puede tener la cobertura.


Cobertura de lineas de código por pruebas unitarias automáticas

¿Qué queremos medir? Si estamos trabajando en un equipo de desarrollo ágil, quizás estemos trabajando con TDD, en cuyo caso estrictamente no deberíamos escribir lineas de código sin que estas correspondan a un test que falle.

En este sentido, si tenemos un equipo que trabaja a conciencia, podríamos asumir que un porcentaje alto de cobertura es la consecuencia esperable de tener buenos casos de prueba y que estamos siguiendo la práctica de TDD.

Pero TDD está muy asociado a refactoriong, y el refactoring aplica tanto al código del producto como al código de la prueba. Podemos hacer refactoring tranquilos, siempre que los test sigan corriendo verdes, no?

Hagamos un experimento:

  1. Tomo mi proyecto estrella, con buena cobertura de pruebas.

  2. Refactor: quito todo el código innecesario en las pruebas (me refiero a todas las llamadas a assertXXX)

  3. Corro las pruebas. Pasan (verde) y ¡¡con la misma cobertura que antes!!


¿Qué falló?

Luego de eliminar los assert mis pruebas son apenas mejores que un compilador con checkeo de tipos (tema para alguna tesis :) ).

Entre la prueba automática y el producto se mantiene una coherencia y sincronización, justamente esto es lo que nos permite decir que, a diferencia de los diseños en documentos, las pruebas automatizadas deben estar siempre actualizadas. Pero la relación entre el código del producto y las pruebas no es simétrica. Si tengo pruebas de mala calidad, el código del producto no las detectan.

¿Cómo podría medir la calidad de mis pruebas? Este es un tema muy interesante, y una razón por la que deberías tener testers en el equipo :D. Como ejemplo, algo que se puede hacer es mutation testing: modificar el producto (aka inyectar errores) para ver si las pruebas detectan los errores inyectados.

Otras coberturas por pruebas unitarias automáticas

En herramientas de medición de cobertura se pueden medir distintos tipos de cobertura. Por ejemplo en Java (con EMMA) se puede medir cobertura por paquete, por clase o por línea. También se podría medir cobertura por función o por expresión. Todas estas, con la idea que cobertura significa que en algún momento se haya ejecutado esa parte del código.

Sin intentar hacer un tratado sobre coberturas, nombro otras:

  • Flujo de datos: el comportamiento puede depender de los datos. En el ejemplo, si tengo una sola prueba assertEquals(2,dividir(4,2)); tengo 100% cobertura pero no estoy probando el caso relevante dividir(x,0)

Código a probar: int dividir(int a, int b) { return a/b; }
  • Camino: si consideramos cada bifurcación del código como un nodo, cada camino en el grafo resultante puede ser significativo desde el punto de vista de prueba. El problema es que el número de caminos crece exponencialmente.

  • Cobertura de requerimientos: damos vuelta la métrica, dado que no podemos probar cobertura de código que no existe (tendríamos cobertura mayor que 100%), medimos la cobertura de los requerimientos para ver cuales está implementados. Algo parecido pasa con las API cuando son impuestas (podemos medir si cumplimos con el contratos de las APIs).


¿Cuál es la cobertura correcta? Es como preguntar cuanto dura un día. Puede ser simple (24hs) o complicado.

Nota: no puedo dejar de nombrar la cobertura de XML, que puede ser pensada como un tipo de cobertura de Flujo de datos o de requerimientos.


Coberturas por pruebas de integración

Las pruebas unitarias parten del diseño. Es una forma de diseño ejecutable, gracias a eso nos aseguramos que la aplicación cumpla con el diseño.

¿Pero es correcto el diseño? Por ejemplo, cuando extendemos o nos integramos con productos de terceros, tenemos que validar que nuestra comprensión de esos producto sea correcta y, desde el punto de vista de regresión, que ese comportamiento no haya cambiado en el tiempo.

Estuve en proyectos en los que construíamos addin a MS Outlook, o extensiones a Liferay, y la importancia de estas pruebas era tan grande (por la evaluación de riesgo) que se ponía en dudas la conveniencia de hacer pruebas unitarias. En estos casos, en vez de la pirámide tradicional de automatización de la prueba (mucha unitaria, algo de integración, poco de sistema desde interfase usuaria), se tenía niveles similares de prueba unitaria y de integración.

Si quisieramos medir la cobertura de las pruebas de integración, tendríamos que medir incluyendo al producto plataforma (Outlook o Liferay)


Otras pruebas

Y no todo termina ahí. ¿Hemos probado seguridad, robustez, usabilidad, etc?

¿Son todas las pruebas basadas en ejemplos? ¿Cómo medimos la calidad de la prueba cuando probamos con técnicas que usan oráculos no humanos, invariantes y fuzzing?

¿Hacemos el producto correcto?

Comentamos que en ocaciones necesitamos saber si el diseño es correcto. Pero más importante, ¿hacemos el producto correcto?

Ya sea que hagamos pruebas exploratorias o que tengamos las pruebas de aceptación automatizadas, ¿cómo medimos que estas pruebas sean buenas?


Conclusión

Repitiendo lo que dije al principio, prefiero mil veces discusiones sobre cuánto debemos probar antes que si tenemos o no que probar.
Pero es una discusión que tenemos que tener en el equipo. Y como siempre, la respuesta no es única.

Publicar un comentario