June 2010 - Artículos

Hace poco gravé un pequeños video en el que explicaba una realidad que he visto en muchos proyectos respecto de las pruebas unitarias. En síntesis lo que comentaba era que en esos proyectos, los beneficios de las pruebas unitarias no eran visibles mientras que los costos sí lo eran.

En problema aparente era la calidad de las pruebas, pero en realidad, el problema de fondo es la estrategia de hacer las pruebas luego de terminado el código. Por lo general, los programadores escriben piezas de código las cuales, para probarlas, son ejecutadas manualmente varias veces mientras que con el depurador se recorren línea por línea los algoritmos. Una vez que la feature está lista, quieren escribir algunas pruebas pero apenas de empezar se dan cuenta que el código que han escrito no es fácilmente testeable, no contempla la posibilidad de inyectarle las dependencias y probablemente han utilizado muchos de lo “enemigos de las pruebas unitarias”.

Aquí el programador puede tomar un de los siguientes caminos:

  1. Refactorizar el código para volverlo testeable.
  2. Probarlo como está, es decir, si el código toma valores de una tabla de la base de datos, pueden ponerle esos valores en la tabla al iniciar la prueba.
  3. No probarlo en absoluto. Esta es (Test-Never)

Estas decisiones no son libres ya que hay ciertos condicionamientos:

  1. Queda poco tiempo. Todo lo que se pudo ahorrar en depuración ya se perdió y ahora no solo se trata de escribir las pruebas sino que hay que refactorizar algo que “ya está andando” para recién luego escribir las pruebas.
  2. Probablemente esa refactorización no sea algo tan sencillo. Es probable que haya que modificar algo más de código que solo el propio. Esto ocurre cuando hay que lidiar con los métodos estáticos y otras malas yerbas ya presentes en el proyecto.
  3. El resto del equipo ya se ha encontrado en este dilema y la decisión que han toma es un antecedente de peso en la cultura del equipo.

Cual de los caminos toma el programador depende de muchos factores pero lo malo del caso es que ninguna de las tres alternativas conduce a algo bueno. Veamos por qué:

En el primer caso, se consume mas tiempo que el que se hubiese requerido si el código se hubiese hecho testeable desde el principio mediante TDD. Es probable que el programador vea esto como una pérdida de tiempo ya que su código “funciona” pero él tiene que modificarlo para “cumplir” con algo, llámese cobertura de código, número de pruebas, etc.

En cuanto a la segunda alternativa, la de probar sin modificar el código, es sin dudas un camino para realizar pobres pruebas de integración. Solo hay que hacerlas y esperar algo más de un año para ver el daño que que hacen al proyecto, cuanto cuestan y cuán poco sirven.

La tercera opción es la más coherente con el modo de desarrollo que se ha tomado pero es sin dudas una estafa. Si se ha estimado el tiempo necesario para codificar las pruebas y si ha comprometido con el equipo ha escribir pruebas para el código propio y pero no se lo lleva a cabo, entonces hay que sincerar la situación.

No interesa que tan buen programador sea, si no se escriben la pruebas interactivamente con el código se llegará tarde o temprano a esta situación.

En mi último post presentaba una métrica (verdaderamente muy mala) sobre mi productividad en un proyecto realizado completamente utilizando TDD de manera estricta. Esta mostraba aproximadamente 9 LOC/Hs. Al mismo tiempo, y como las pruebas y el código los escribí interactivamente, escribía 11 LOC/Hs de pruebas. Esto hace un total de 19 LOC/Hs.

Ahora bien, cada 2 o 3 pruebas el código era refactorizado para eliminar duplicaciones, del mismo modo que luego de observar un patrón común en un conjunto de pruebas, las mismas se refactorizaban también. Esto sucedió varias decenas, quizás cientos, de veces.

Entonces, cuantas líneas de código son 9 líneas de código cuando el refactoring se hace minuto a minuto?

Es probable que alguien se pregunte: ¿pero, a quien le importan las LOCs?. A mi me importan, porque he comprobado la diferencia en este aspecto entre Test-First y Test-Last (o Test-Never) y esto redunda directamente en calidad.

Hace poco comencé un nuevo desarrollo y decidí grabar algunos videos de los cuales solo publiqué los primeros tres. Sucede que el hecho de saber que alguien me estaba mirando me hacía prestar mayor atención a mis palabras que al código que debía escribir. No obstante a ello, continué grabándome para tomar el tiempo y estudiarme.

La primera parte de ese desarrollo está completado y estos son los números:

66 pruebas unitarias.
15 clases. (solo 4 centrales, el resto son datacontracts, excepciones y helpers)
238 líneas de código.
300 líneas de pruebas.
1,2605 líneas de pruebas por cada líneas de código del producto.
73 es el menor índice de mantenibilidad que obtuve. 88,9 es la media.
97,74% de cobertura total.
26 horas de programación.

9,15 LOC/hora
11 LOC(test)/hora.

Como nunca me medí mi productividad ni conozco la densidad de defectos que tiene este código ni ningún otro que haya escrito, no puedo hacer comparaciones pero sí puedo sacar algunas conclusiones y dar mis apreciaciones personales.

  • TDD te lleva a realizar muchas pruebas. Si eres riguroso y no aceptas introducir ninguna línea de código sin antes tener una prueba que la respalde, terminas escribiendo muchas pruebas.
  • Muy alta cobertura de código, el único código sin pruebas es aquel que ha surgido de las recomendaciones del analizador estático de código con respecto al número de constructores que requieren las excepciones. (No obstante, nada impide que cree pruebas para estos)
  • Más líneas de pruebas que líneas de producto. Esto lo he visto antes pero para el mix “mal código-malas pruebas” pero parece que puede darse en este caso también. Debo aclarar aquí que así como el código se fue refactorizando minuto a minuto, las pruebas también fueron refactorizadas al punto tal que no existe en el proyecto ninguna prueba de más de 9 LOCs. Incluso puede verse en las capturas que existe una jerarquía de clases para las pruebas.
  • Aún el menor índice de mantenibilidad es elevadísimo. Si no me crees, obtén las métricas de tu proyecto actual.
  • Realmente no sé si 9.15 LOC/hs es algo bueno o no. A mi entender, es un número excelente ya que , haciendo un cálculo infantil (lineal), representarían casi 1.500 LOCs/mes. Ahora que si contemplamos las pruebas como algo de valor y las agregamos daría 3224 LOCs/mes, lo que a mi parecer es sumamente bueno.

 

image 

image

image

image