TDD como metodología de diseño de software

TDD o Test-Driven Development (desarrollo dirigido por tests) es una práctica de programación que consiste en escribir primero las pruebas (generalmente unitarias), después escribir el código fuente que pase la prueba satisfactoriamente y, por último, refactorizar el código escrito. Con esta práctica se consigue entre otras cosas: un código más robusto, más seguro, más mantenible y una mayor rapidez en el desarrollo. En este post voy a centrarme solamente en como TDD afecta al diseño de software, si queréis más información, hay una introducción bastante buena en la Wikipedia y Carlos Blé tiene disponible online un libro muy completo.

Antes pensaba que TDD era una forma de programar que consistía en generar primero los tests unitarios antes que la propia aplicación, con lo que conseguías desarrollos de más calidad a costa de disminuir la productividad. Creo que es la misma idea que tiene la mayoría de la gente que conoce por encima esta práctica, pero que no se anima a utilizarla. Sin embargo, últimamente he estado profundizando un poco en TDD y me he dado cuenta que esto no es cierto, TDD no es para hacer pruebas, es una práctica que envuelve el desarrollo en su conjunto, especialmente el diseño de software. De hecho, algunos dicen que su última letra, debería significar diseño y no desarrollo. Es decir, diseño orientado por las pruebas.

TDD fue creado por Kent Beck (quien también inventó Extreme Programming y JUnit), y en esencia, es un proceso a seguir, lo cual ya lo hace diferente a un simple enfoque de pruebas primero:

Test driven development
Fuente de la imagen: Wikipedia

 

Este ciclo también se lo conoce como rojo (hacer que la prueba falle), verde (hacer que la prueba pase) y refactor. Aunque al principio pueda parecer muy parecido a un enfoque de probar primero, al combinarlo con prácticas de desarrollo ágil, TDD toma un enfoque mucho más amplio, y cambia su atención de las pruebas al diseño. TDD está mucho más relacionado con el diseño emergente que con las pruebas, de hecho, que TDD genere una gran cantidad de pruebas es un efecto secundario positivo, pero no es su propósito final.

El proceso de diseño de software, combinando TDD con metodologías ágiles, sería el siguiente:

  1. El cliente escribe su historia de usuario
  2. Se escriben junto con el cliente los criterios de aceptación de esta historia, desglosándolos mucho para simplificarlos todo lo posible
  3. Se escoge el criterio de aceptación más simple y se traduce en una prueba unitaria
  4. Se comprueba que esta prueba falla
  5. Se escribe el código que hace pasar la prueba
  6. Se ejecutan todas las pruebas automatizadas
  7. Se refactoriza y se limpia el código
  8. Se vuelven a pasar todas las pruebas automatizadas para comprobar que todo sigue funcionando
  9. Volvemos al punto 3 con los criterios de aceptación que falten y repetimos el ciclo una y otra vez hasta completar nuestra aplicación

Vamos con un ejemplo práctico de este ciclo:

  1. Supongamos que el cliente nos pide que desarrollemos una calculadora que sume números (es lo primero que se me ha ocurrido).
  2. Acordamos con el cliente que el criterio de aceptación sería que si introduces en la calculadora dos números y le das a la operación de .suma, la calculadora te muestra el resultado de la suma en la pantalla.
  3. Partiendo de este criterio, comenzamos a definir el funcionamiento del algoritmo de suma y convertimos el criterio de aceptación en una prueba concreta, por ejemplo, un algoritmo que si introduces un 3 y un 5 te devuelve un 8:
    public void testSuma() {
    	assertEquals(8, Calculadora.suma(3,5));
    }

    Este punto es para mí el más importante del TDD y que supone un cambio de mentalidad, primero escribo cómo debe funcionar mi programa y después, una vez lo tengo claro, paso a codificarlo.

    Al escribir el test estoy diseñando cómo va a funcionar el software, pienso que para cubrir la prueba voy a necesitar una clase Calculadora con una función que se llame Suma y que tenga dos parámetros. Esta clase todavía no existe pero cuando la cree, ya sé cómo va a funcionar. Este caso es muy trivial, pero muchas veces no sabemos exactamente qué clases hacer o qué métodos ponerle exactamente. Es más, a menudo perdemos el tiempo haciendo métodos y clases que pensamos que luego serán útiles, cuando la cruda realidad es que muchas veces no se van a usar nunca. Con TDD sólo hacemos lo que realmente necesitamos en ese momento.

    Realmente es la forma natural de pensar, primero pensamos en “qué” queremos hacer y después pasamos al “cómo”, la diferencia es que con TDD el test ya queda escrito y se ejecutará cada vez que compilamos nuestro programa.

  4. Por supuesto, si intentamos pasar este test nos dará un error, porque la clase Calculadora aún no existe.
  5. Ahora pasamos a escribir el código de la clase, es fácil porque ya sabemos exactamente cómo se va a comportar:
    public class Calculadora {
    	public static int suma (int a, int b) {
    	int c = a + b;
    	return c;
    	}
    }
  6. Ahora ejecutamos la prueba y ya tenemos el código funcionado con la prueba pasada.
  7. Una vez todo esté funcionando, pasamos a refactorizar y a eliminar código duplicado, este ejemplo es extremadamente sencillo, y en un caso real no haríamos tantos pasos para algo tan evidente, pero el código mejorado podría ser por ejemplo:
    public class Calculadora {
    	public static int suma (int a, int b) {
    	return a+b;
    	}
    }

    En ejemplos más complejos, según vayamos escribiendo más test, deberíamos buscar código duplicado y agruparlo en funciones o utilizar la herencia o el polimorfismo.

  8. Es importante pasar todos los test después de refactorizar por si nos hemos cargado algo.
  9. Ahora deberíamos volver al punto 3 con tests más complicados y repetir el proceso, por ejemplo, podíamos pasar a que el algoritmo admita sumar números decimales, etc.

Esta forma de trabajar es también muy buena para entender el código. Sabemos que la calidad del diseño de un software está también relacionada con el conocimiento del equipo de desarrollo en relación al dominio en cuestión. En este sentido, las pruebas son una muy buena forma de entender el código y su funcionamiento, muchas veces incluso mejor que la documentación.

También hay que decir que no todo es perfecto en TDD, cuando llegue el momento de crear un test sobre la interfaz de la calculadora la cosa se complica. Los puntos flojos que veo en TDD son:

  • Hay que utilizarlo y entenderlo bien para que sea realmente productivo, te ayuda a centrarte en lo importante y a no sobrediseñar, pero es importante saber refactorizar el código según vaya evolucionando para que sea consistente
  • Pruebas sobre interfaces gráficas. Aunque hay soluciones parciales propuestas, para mí TDD solo funciona en la capa de negocio, no encaja con interfaces visuales
  • Bases de datos. Hacer pruebas de código que trabaja con base de datos es complejo porque requiere generar unos datos conocidos antes de hacer las pruebas y verificar que el contenido de la base de datos es el esperado después de la prueba. Los objetos simulados (MockObjects) son otra opción, pero personalmente creo que se pierde tiempo con esto

Mi rol es diseñar proyectos de Internet y hacer que salgan bien. Es lo que me gusta hacer y a lo que me he dedicado los últimos 10 años. Siempre en busca de nuevos retos, me interesan temas tan diversos como las metodologías ágiles, las tecnologías Cloud o el diseño de producto. Para hacer buenos productos, procuro siempre crear un marco de trabajo que permita a las personas mejorar y dar su mejor versión.

Ver toda la actividad de José Ignacio Herranz Roldán

Recibe más artículos como este

Recibirás un email por cada nuevo artículo.. Acepto los términos legales

Posts relacionados

13 comentarios

  1. gnz/vnk dice:

    Sé que sólo es un ejemplo, pero creo que es muy importante escoger bien los ejemplos que se usan. Si tratas de presentar las bondades de algo y pones ejemplos que no sólo no reflejan bien esas bondades sino que tienen otros problemas adicionales, no haces ningún bien a tu exposición.

    >”criterio de aceptación en una prueba concreta, por ejemplo, un algoritmo que si introduces un 3 y un 5 te devuelve un 8 […] Este punto es para mi el más importante de TDD”

    Para mi, que no practico mucho TDD, también es el punto más importante. Sin embargo, es importante porque puede suponer la diferencia entre hacer Test Driven Design y hacer Random Example Driven Design. Que una función, a la que le pasas un 3 y un 5 te devuelva 8 no implica, ni de lejos, que la función sea una función de suma.

    Dicho de otro modo, si no escogemos buenos tests y un buen sistema de tests, el código obtenido finalmente es tan frágil como podría serlo con otra metodología. Únicamente estarán cubiertos un número de casos triviales que con seguridad cubriríamos fácilmente de todos modos.

    De hecho dices “hemos probado que el algoritmo de suma funciona bien” pero la realidad es que no, eso no es cierto en absoluto. Lo único que hemos probado es que, recibiendo 3 y 5, el método devuelve 8.

    Por eso es importante señalar que no podemos confiar en que el simple hecho de hacer algunos tests hace que el código sea más robusto. Para conseguir eso tenemos además que esforzarnos en que los tests elegidos realmente reflejan los requerimientos necesarios para garantizar que el funcionamiento es correcto.

    Y es evidente que en un caso como este, no podemos escribir infinitos tests como (3,5)->8, (3,6)->9, (4,2)->6, etc. Necesitamos _diseñar_nuestros_tests_ de una forma más razonable y efectiva.

  2. Javier dice:

    Muy buena introdución a TDD. Donde laboro yo, usamos RUP, pero yo he ido intentando implementar algunas de las prácticas de TDD como codificar primero el test y posteriormente el desarrollo. Lo que si me ha parecido bastante complicado, es como tu lo dices, hacer pruebas sobre bases de datos o web services. Las cosas se complican aún más cuando estas se automatizan y hay que hacer rollback permanentemente. Estoy considerando lo de los Mocks, pero no se que tan rentable y productivo sea dedicarle tiempo a diseñarlos. Amanecera y veremos.

    Salu2!!!!!

  3. Nacho dice:

    gnz/vnk: gracias por tu comentario, tienes razón que únicamnente con ese test no garantizas que el algoritmo de suma funcione bien, es solo el primer test para validar el criterio de aceptación, espero que haya quedado clara la idea a pesar del ejemplo.

    Un saludo.

  4. Nacho dice:

    javier: estoy de acuerdo contigo. En cuanto a los Mocks puedes probar con Mockito http://code.google.com/p/mockito/ que es bastante sencillo.

    Un saludo

  5. Alfredo Casado dice:

    Me gusta ver que el interés por TDD se va extendiendo. Sobre el ejemplo, realmente si haces un test que sume 3+5 lo único necesario para hacerlo pasar es escribir “return 8”. Y esto que puede parecer una chorrada no lo es, algo fundamental en TDD es escribir el código más simple posible que satisfaga las pruebas. Ahora estas obligado a escribir nuevas pruebas que te “fuerzen” a escribir la verdadera implementación. De ese modo TDD te habrá ayudado a encontrar la solución más simple y además tus test realmente verifican que la clase funciona.

    Sobre los puntos flojos de TDD:

    – El primero no es realmente un punto flojo, nadie dijo que fuera fácil. Realmente las cosas suelen ser así, las cosas que realmente aportan valor cuesta esfuerzo.

    – El tema de los UI, para web hay soluciones aceptables como selenium, para swing tienes cositas. Se puede hacer, es más costoso, pero se puede y normalmente compensa.

    – Las pruebas de BD no son mucho problema, simplemente tienes escribir primero los datos que requiera tu test y luego es un test normal. Para esto los mocks no tienen mucho sentido, dado que tu lo que probablemente quieras probar es que tu código funciona correctamente integrado con la BD (son más bien test de integración que unitarios), los mocks son útiles en otros muchos contextos. A ver si termino de escribir un post que tengo pendiente sobre mocks que creo que es un tema que la gente no tiene muy claro.

  6. gnz/vnk dice:

    @Alfredo Casado

    Esto me gusta. Sigamos entonces con ese argumento…

    Tu primer test es que suma(5,3) devuelve 8. Así que hacemos un return 8 a saco y hop! test verde! Ahora dices que debemos escribir nuevos tests que te fuercen a escribir “la verdadera implementación”.

    Bien, estupendo. Entonces, si el test suma(5,3) -> 8 es un buen primer test. ¿Cuál es el siguiente test a escribir? ¿Será otro test del mismo estilo? ¿Por ejemplo que suma(5,2) -> 7? ¿O sería un test de otro estilo, quizá más genérico? ¿Qué test propones que sería el siguiente?

  7. Alfredo Casado dice:

    Hombre era un ejemplo, en realidad no empezaría con 5+3. Lo lógico es empezar por el caso más simple posible, por ejemplo 0+0, y luego 0+1. Es te caso es tan simple que con estos dos test ya sale la implementacion general, ¿cuando escribes el test de 0+1 que opciones tienes para hacer que pase?

    – a + b o bien

    – if (b== 0) return 0 else return 1

    La primera expresión es más simple que la segunda, así que ya hemos terminado (este caso es extremadamente simple claro). A este proceso de buscar los test que nos sirvan para llegar a la implementación se le suele llamar triangulation por cierto. Puedes buscar más info sobre esto si te interesa o mirar algún video con katas resueltas que suelen ser muy ilustrativas de como un experto en tdd va seleccionando los casos de test y avanzando en “baby steps”

  8. gnz/vnk dice:

    @Alfredo Casado

    Pues precisamente a eso iba. A que es muy diferente probar un caso seleccionado por alguna razón particular, a probar un caso cualquiera al azar. Y a que justo es ahí donde está “la clave”.

    Por otra parte olvidas una opción más sencilla aún, que cumple (0,0)->0 y (0,1)->1, que es “return b” que es **más sencillo** que “return a+b” ;)

    ¿Qué pasa? Que sabes claramente a dónde quieres llegar y te has saltado pasos precisamente por que lo sabes. Otra razón más por la que es un mal ejemplo.

  9. Nacho dice:

    Me alegra saber que hay gente interesada en TDD. De todas formas para mi lo importante del post no era profundizar en TDD, ni como selecionar los mejores casos de test, sino explicar que TDD sirve para diseñar software, y quitar a la gente de la cabeza que TDD es una metodología de pruebas.

  10. gnz/vnk dice:

    @Nacho

    Siento mucho si he desviado el tema a otra cosa. Lo único que quería señalar es que esa “clave” que tú ves, ese “primero escribo lo que debe hacer”, debe ir más allá, es más profundo que el simple hecho de escribir un test primero y pasarlo después. El diseño no es el hecho de hacer el test *primero*. La *selección* de un test o de otro *es* el diseño.

    De todos modos, lo dicho, siento haber desviado el tema o desvirtuado de alguna forma el argumento original.

  11. Nacho dice:

    @gnz/vnk

    No me importa que se desvía la conversación, además estoy totalmente de acuerdo contigo, haría falta una secuencia de test y no un solo test para entender bien como el diseño va tomando forma y entender también la importancia de la refactorización que en un ejemplo tan simple tampoco queda claro, lo que pasa es que tampoco quería extenderme demasiado

Escribe un comentario