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.

6 comentarios:

Abel Muiño dijo...

Es un buen título para captar lectores :-). Aunque creo que sería más realista decir "El % de cobertura, por sí solo, no significa (casi) nada".

Creo que no es una crítica al % de cobertura, si no a cualquier métrica aislada. Todas las pruebas que comentas al final tampoco significan nada.
¿Para que sirven unas pruebas completas de carga sin pruebas funcionales?

Por otra parte, la cobertura sirve para indicar qué parte del código desarrollado va a ser ejecutado por primera vez por el cliente... Eso es algo (no todo, quizá ni siquiera es mucho en el mundo ideal, pero para muchas empresas saber eso es todo un adelanto).

Si, las pruebas con las que se calcula la cobertura pueden ser malas, regulares o buenas, pero "asustar" a quienes intentan implantar algo que les permita ir aprendiendo y mejorando creo que es peor. Si no se da el primer paso, no se recorre el camino.

Quedo a la espera de una segunda parte "Cómo hacer que el % de cobertura signifique algo" :-)

Juan Gabardini dijo...

Hola Abel
Me declaro culpable de buscar un título con gancho!
Estoy de acuerdo que una métrica aislada no sirve en sí misma. Sólo con el valor no podemos saber si es bueno o malo. Lo importante surge de preguntar: ¿por qué dio ese valor?

"cobertura sirve para indicar qué parte del código desarrollado va a ser ejecutado por primera vez por el cliente."
No, sirve para saber que parte del código se ejecutan con tus pruebas. Aunque se puede grabar las sesiones de los usuarios, eso es lo que hacen algunos productos que preguntan si se puede enviar información de uso.

¿Asustar? No fue mi intensión. Aunque una amiga me anticipó (y no te hice caso , Colo!) que el post apunta a una audiencia limitada: testers técnicos y programadores en equipos de desarrollo de software con TDD y con alto porcentaje de cobertura.

En otros contextos, esta discusión puede estar fuera de lugar, ya que lo que se discute es, por ejemplo, si hacer o no pruebas unitarias o TDD.

Me gustaría hacer la segunda parte, como tu pides, aunque creo que en el mejor de los casos será válido en un contexto particular, no como una solución general. Ese es el mensaje que intento transmitir.

Gracias por leer y comentar

Unknown dijo...

Juan,

Concuerdo con lo de metrica aislada, conjungando diferentes metricas es una mejor opcion,
por ejemplo, usando Sonar,
http://sonar.codehaus.org/features/

Plus el tema de la mejora o no en funcion del tiempo, usando la opcion TimeMachine de dicha herramienta, una vez que dispones/utilizas algo como esto, se tiene una base solida para la
mejora continua de los proyectos
(comparativa tambien)


Podes ver una instancia live de
esta herramienta en:


http://nemo.sonarsource.org/project/index/org.apache:tomcat

Juan Gabardini dijo...

gracias por el link Massello, no lo conocía. Saludos

Sergio Bogazzi dijo...

Gracias por el articolo Juan. Otra cosa a considerar cuando se trata de determinar si el equipo ha "construido lo correcto" (por ejemplo, la cuestión de la validación) es que el Behavior Driven Development, donde cada scenario y feature está escrito por un usuario, mapas de 1:1 a la cuestión de la cobertura. Por ejemplo, si usted tiene 5 escenarios de un total de 10 escritos por el usuario, cuando el sistema supera con éxito cada uno de ellos, sabemos un poco más sobre la cuestión de la validación.

Juan Gabardini dijo...

Gracias Sergio.
Si, no está claro en el post, pero la cobertura por requerimientos estaría ligada a BDD y a la pregunta "hacemos el producto correcto"