Mar 11 2010

Reflexiones sobre los problemas del desarrollo orientado a pruebas

Categoría: Test UnitariosJuan @ 2:45 pm

Hace tiempo llegó a mis manos un enlace sobre TDD (si no recuerdo mal, en un comentario a alguna anotación del blog de JMB), dónde el autor explicaba los problemas de TDD y por qué, en su opinión, hacer TDD no merece la pena. Hoy voy a escribir un poco sobre esto y a dar mi punto de vista sobre TDD y sobre lo que el autor comenta sobre TDD.

El autor comienza exponiendo que no está demostrado por la comunidad científica que TDD aporte mejoras significativas en el desarrollo del software. Como ejemplo cita a un par de párrafos de Maria Siniaalto en su artículo Test-Driven Development: empirical body of evidence. De esos dos, me quedo con el siguiente:

The empirical evidence on the practical use of TDD and its impacts on software development are still quite limited.

[La evidencia empírica en el uso práctico de TDD y su impacto en el desarrollo del software esta, todavía, muy limitada]

Con este soporte, pasa a dar su opinión que defenderá en el resto del texto:

I mention this first because I’ve concluded that not only is TDD not useful for me but I don’t think it’s a generally useful technique

[Menciono esto primero porque he concluido que no sólo TDD no es útil para mi, sino que no creo que sea una técnica útil en general]

No hace mucho, en la lista de correo de TDD en español (en la cual os invito a participar si estáis interesados en este tema) hablamos de lo mismo debido a otro articulo diferente. Mi punto sigue siendo el mismo que en aquel entonces (de hecho, he reutilizado algunas frases). Un estudio para ver si TDD mejora o no la calidad, rendimiento, etc., tiene que tener muchísimas variables en cuenta. Se me ocurren tres escenarios:

  • Proyectos distintos (con y sin TDD), gente distinta. La comparación no es posible porque la gente es distinta, los proyectos son distintos, los equipos son distintos, los comportamientos son distintos, etc.
  • Mismos proyectos (con y sin TDD), misma gente. Si ponemos primero a gente en un proyecto sin hacer TDD y se estudia, y luego se repite el estudio con la misma gente haciendo TDD, tampoco es posible hacer una buena comparación porque los individuos ya tienen conocimiento y experiencia en el proyecto.
  • Proyectos distintos (con y sin TDD), misma gente. Igualmente habría muchas variables dentro de los proyectos que podr�an influir en el resultado del mismo (complejidad, asuntos personales, ambiente…).

Según lo veo yo, para hacer un estudio de estas características se necesitan un número suficientemente grande de gente y proyectos para que sea estadísticamente significativo (y eso es mucho dinero y mucho tiempo). No creo que vaya a pasar y por tanto siempre habrá unos artículos dónde salga que TDD es mejor y otros en los que se diga que TDD es peor ya que, como se ha dicho, hay muchos otros factores influyen en el resultado.

Lo importante para mi no es que haya artículos de investigación que “demuestren” las maravillas o la perdida de tiempo que supone hacer TDD. Lo que a mi me importa es lo que yo veo y siento cuando hago TDD y lo que, por mi experiencia, veo en equipos e individuos cuando hacen TDD. Personalmente, a mi me sirve para hacer mejor código, más claro y más testeable. Mi código es mejor, mi actitud es mejor, me obligo a pensar más en las pruebas, tengo más confianza en que lo que he hecho funciona y, además, disfruto haciéndolo.

En el artículo mencionado al principio, el autor dice que TDD no da confianza en que el código funcione. La explicación es que en TDD no se pueden añadir pruebas que no hagan fallar código:

TDD by itself cannot give you that confidence because it excludes the idea of adding tests which are expected to pass

[TDD por sí mismo no puede darte esa confianza porque excluye la idea de añadir pruebas que se espera que pasen]

Estoy completamente en desacuerdo. Es más, no estoy del todo seguro de que, incluso siendo purista, no se puedan añadir pruebas que no hagan fallar el código. Robert C. Martin (Uncle Bob) escribe en su artículo “Las tres reglas de TDD“, la siguiente regla número dos: “You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures“. Esta frase se puede interpretar como que una vez que has escrito una prueba que haga fallar el código, no esté permitido escribir ninguna otra prueba más, pero eso no tiene que significar que todas las pruebas que se escriban deban fallar en un principio. Es más, Kent Beck en su libro Test-Driven Development by example habla de sólo dos reglas fundamentales antes de presentar más:

  1. Write new code only if an automated test has failed [Escribe nuevo código sólo si una prueba automática ha fallado]
  2. Eliminate duplication [Elimina la duplicidad]

Ninguna de estas reglas supone una contradicción al escribir pruebas que pasen. Si lo miramos desde un punto de vista práctico, no hacemos más que asegurarnos de que ese caso pasa, ¿qué tienes esto de malo?, ¿por qué deberíamos borrarlo o no usarlo si pasa?, ¿por qué debería estar prohibido?. Cuando escribimos código para hacer pasar un test, escribimos el código más simple que podemos pensar para hacer pasar la prueba, sin embargo, esto no tiene por qué conllevar que ese “código mínimo” solamente hace pasar ese test específico. Algunas personas que hacen TDD dicen que lo que habría que hacer es “romper el código” artificialmente para ver cómo la prueba falla y después “arreglarlo” para verla pasar. En mi opinión esto es una pérdida de tiempo y ganas de hacer que una buena práctica parezca una religión. (Incluso el diagrama de flujo en la wikipedia, no ve contradicción en escribir una prueba que pase :-) )

El autor también habla de que TDD no considera el peor caso o casos límite. ¿Cómo?. Claro que no, esto es puramente dependiente del programador (igual que lo es si hacemos pruebas al final), de lo cuidadoso que sea con las pruebas y de cuanto piense en los casos que necesita. No obstante, hay una gran ventaja al hacer TDD y es que para que escribir el código tienes que escribir las pruebas antes y eso te garantiza, al menos, cierto número de casos (¿cuantos se garantizan al hacer pruebas al final?). Obviamente, hay que pensar y esforzarse, eso no viene gratis por el hecho de TDD. Sin embargo, en mi experiencia es más fácil olvidarse de un test cuando ya tienes el código escrito que cuando todavía está por escribir y tienes que pensar en casos y comportamientos. Habla también del rendimiento y de cómo TDD no se centra en ello. Estoy de acuerdo en que TDD no es la mejor aproximación a la programación de algoritmos pero eso no quita para que no se pueda hacer. Igual que se hacen otro tipos de pruebas, se pueden añadir pruebas de rendimiento y mejorarlo en la fase de refactorización sin cambiar el comportamiento. ¡Para eso tenemos las pruebas!

Hay algunas cosas más de las que habla el autor, pero estas eran para mi las más importantes y en las que me quería enfocar y rebatir. Hay una idea que sí que nunca había oído antes que me ha llamado mucho la atención y en la que creo que merece la pena reflexionar:

If I write the tests first, I also worry that I’ve overfit my code to the tests. This is a problem that happens in statistical modelling. Given any set of data points, I can fit them to a model. The next question is, is the model valid and useful? The way to check is to use them to make predictions, and see how well it matches reality. This in turn means testing the model with data which wasn’t used to make the model.

[Si escribo pruebas primero, también me preocupo de que he me he pasado dando forma al código con las pruebas. Este es un problema que ocurre en modelos estadísticos. Dado cualquier conjunto de puntos de datos, puedo encontrar un modelo que le corresponda. La siguiente pregunta es, ¿es el modelo válido y útil?. La manera de verificar esto es usar el modelo para hacer predicciones y ver cómo de bien refleja la realidad. Esto significar probar el modelo con datos que no han sido usado para crear el modelo]

En principio, esa es una de las grandes ventajas de TDD, el modelado del código mediante las pruebas, pero, ¿es posible que debido a las pruebas estemos haciendo un modelo que se ajuste a las pruebas pero no al comportamiento general? Es posible y realmente merece la pena mirar los modelos estadísticos para entenderlo un poco mejor (lo tengo pendiente ya que la estadística la tengo muy olvidada) (supongo que este es el problema de hacer modelos de dominios infinitos con un número finito de datos). Sin embargo, creo que es obvio que TDD no es la panacea y que si usamos TDD todavía necesitamos usar otras prácticas. El código, después de todo, debe ser sujeto a pruebas de sistema, de estrés, de rendimiento, de exploración que, por otro lado, están fuera del modelo bajo el cual se ha escrito el código.

Como resumen, decir que creo que es estúpido el tratar a TDD como una religión. Pero es igualmente estúpido el tratar de ser “anti-TDD” como religión. No creo que es lógico decir que TDD es la solución a todos mis problemas, al igual que no es lógico decir “TDD es una práctica que no sirve para nada si usas otras prácticas”. Esto no llega a ningún lado y el contexto influye en las prácticas que hay que tomar y cuando utilizar una u otra. En mi experiencia, he visto gente y equipos que han mejorado mucho (sobre todo en número de errores bugs) al empezar a hacer TDD mientras que otros han hecho el mismo código malo con el añadido de que un montón de pruebas horribles. También he visto equipos con gente muy metida en tests (haciéndolos después del código) que al tomar TDD en práctica no han notado mejoría en número de bugs o en rendimiento. Al final, lo más importante es tener buena gente en el equipo que tengan actitud profesional, buenas aptitudes y ganas de mejorar.

P.D.: Dejo algunos enlaces muy interesantes que he estado leyendo últimamente sobre TDD y algoritmos.

http://www.infoq.com/news/2007/05/tdd-sudoku

http://www.reddit.com/r/programming/comments/9sdcm/tdd_sudoku_i_took_a_stab_at_it_see_inside_part_1/

http://norvig.com/sudoku.html

Etiquetas:

8 Responses to “Reflexiones sobre los problemas del desarrollo orientado a pruebas”

  1. Joserra says:

    Totalmente de acuerdo, Juan. Y tu experiencia en estos temas para mi vale más que algunos estudios estadísticos (o la falta de ellos) ;)

  2. Yeray Darias says:

    Muy buen artículo y bien argumentado que es lo importante. Estoy totalmente de acuerdo contigo y principalmente destaco varios puntos que has mencionado, como la necesidad de tener un equipo profesional, ya que cualquier metodología o técnica es mala si el equipo es malo. También estoy de acuerdo en que TDD no prohibe hacer tests que no fallen, pero aunque así lo hiciese, podrías hacer TDD y además escribir otros test que te convienen por cuestiones de mantenibilidad y no habría ningún problema, o al menos esa es mi opinión (las herramientas están para usarlas según su conveniencia, como has dicho no hay que tomarselo como una religión).

  3. Jordi Salvat i Alabart says:

    No necesitas estadística para entender el problema del “overfit”. Simplemente dibuja dos ejes (x e y) y 4 puntos más o menos (pero no del todo) alineados. Ahora busca una función polinómica que pase por los 4… salvo casos especiales, necesitarás una cúbica. Dibújala… y comprueba cuan poco se parece a tu intención inicial de que los puntos estuviesen más o menos alineados. Te has pasado de grado: tu curva pasa por los 4 puntos, pero fuera de esos puntos no guarda relación alguna con ellos. Una recta pasaría como máximo por 2, pero reflejaría la naturaleza de los puntos mucho mejor.

    Las metodologías iterativas pueden inducir este mal — tanto más cuanto más pequeña es la iteración. Y TDD es, de todas las que se proponen, la de iteraciones más pequeñas.

    Pondré un ejemplo algo chorras, pero espero que ilustrativo:

    Un programador recibe esta petición: “por favor programa la secuencia de Fibonacci, que es: 0 1 1 2 3 5 8 …: una función fib(n) que devuelva el n-ésimo elemento de la secuencia.”

    El programador aplica TDD en todos los casos que se le ocurren (como el problema es complicado para él y no entiende su fondo, solo se le ocurren los que le han dado):

    int fib(int n) {
    switch (n) {
    case 0: return 0;
    case 1: return 1;
    case 2: return 1;
    case 3: return 2;
    case 4: return 3;
    case 5: return 5;
    default: return 8;
    }
    }

    El código se ajusta a todos los puntos, pero es mucho más complejo (por lo menos en términos de complejidad ciclomática o de líneas de código) de lo necesario y… fuera de esos puntos no tiene nada que ver con la secuencia de Fibonacci.

    He visto equipos que han hecho lo mismo a un nivel de complejidad más alto: resolver cada caso específico por separado sin conseguir nunca generalizar/abstraer, y acabar escribiendo 76 mil líneas de código para una aplicación que no necesitaba más de 25 mil (números procedentes de un caso real).

    P.D.: conste que con esto no pretendo ni respaldar ni contradecir al autor. Aún no he conseguido aprender a hacer TDD en proyectos reales, y hasta que no lo haya probado en mi propia carne no pienso opinar.

  4. Juan says:

    Hola Jordi.

    ¡Muchas gracias por tu comentario! Además, ha quedado muy claro lo del “overfit”. Con comentarios así da gusto.

    Tu ejemplo viene a reforzar mi último comentario sobre las aptitudes de las personas, sin embargo no creo que esté relacionado con las iteraciones y no veo que sea un problema que proviene de ellas.

    Sé que es un ejemplo muy simple pero si un programador da esa solución es que: o es un o mal programador (haciendo TDD o no, eso es lo de menos) y el problema primario reside en que ni siquiera ha entendido el problema (y ni siquiera lo sabe o no se da cuenta), que el problema está mal especificado y da una idea errornea de lo que es (el programador entiende el problema presentado perfectamente, pero no es el mismo que el problema “real”), o que aún sabiendo que no está todo claro, no puede o no se atreve a preguntar y clarificar con el “cliente” el problema. ¿Cómo ayudaría el no hacer TDD o el no usar iteraciones en este caso? En mi opinión, el resultado sería el mismo porque el problema no está en qué práctica se usa, sino en el entendimiento, aptitudes y actitud (no pregunto aunque no entiendo el problema bien, a ver si me van a tachar de tonto) de la persona que trabaja en ello.

    El probelma en este caso no tiene nada que ver con TDD u otras prácticas. Es un problema más de raíz como hemos visto. La misma mala solución se podría haber dado haciendo pruebas al final, haciendo un diseño complejo al principio, usando diseño emergente, sin hacer pruebas…

  5. Juan says:

    Jordi,

    un paso más allá después de lo que has explicado (todavía le sigo dando vueltas :-D).
    Creo que lo más interesante viene del caso en que el problema sí es entendido pero las pruebas (al ser casos puntuales) modelan una solución incompleta. Ahí es dónde el comentario del autor del primer artículo me parece más interesante. ¿Se te ocurre algún ejemplo para este caso?

    Saludos,

    Juan

  6. Leo says:

    Muy interesante el artículo, y el ejemplo de Jordi de Fibonacci. Aunque a lo mejor es un poco off-topic, a veces pasa lo contrario que comentas, que la gente no profundiza suficiente porque cree que ya lo sabe. P.e., para los número de Fibonacci, normalmente se piensa que para calcular el número N hay que calcular todos los anteriores, pero se puede calcular un número suelto usando una fórmula: http://upload.wikimedia.org/math/1/7/2/1725c4014413c474791dae0fb8f3b7e4.png siendo la razón aúrea: http://upload.wikimedia.org/math/2/9/4/2945e3f774483f06d73f20f9d1bf094c.png

  7. Juan says:

    Siguiendo tu comentario, Leo, en este enlace http://www.reddit.com/r/programming/comments/9sdcm/tdd_sudoku_i_took_a_stab_at_it_see_inside_part_1/ (que está en la anotación) se muestra como resolver un sodoku haciendo TDD.
    El autor dice lo siguiente y creo que acierta de pleno:

    Before you try to write code, you have to understand the problem (duh). This usually involves reading, thinking, lots of scraps of paper and time standing around the whiteboard.

    Creo que esta es una de las diferencias entre el fallo de Ron Jeffries al intentar resolver el mismo problema de los sodokus y el del ejemplo del enlace.

    Saludos,

    Juan

  8. Carlos Ble says:

    Muy bueno el post y los comentarios Juan. Enhorabuena :-)

Leave a Reply