Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace https://old.chuidiang.org

TDD: Test Driven Development

Cuando hacemos una aplicación, es muy importante probar el código. Una solución es ejecutar muchas veces la aplicación cuando está acabada, pero especialmente mientras la estamos desarrollando, y probar mucho, muchas veces, una y otra vez. Otra solución más eficiente (y menos aburrida), consiste en hacer test automáticos de prueba, que se ejecuten automáticamente cada vez que compilamos. De esta forma, sin que nos cause ningún trastorno, nuestra aplicación se estará probando sola una y otra vez.

Y la mejor forma de asegurarnos que escribimos estos test automáticos y que la mayor parte de nuestro código se prueba, consiste en hacer primero los test. NO hacemos código y luego el test que lo prueba, sino que primero pensamos una cosa concreta que tiene que hacer nuestra aplicación, luego hacemos el test que prueba que nuestra aplicación hace eso y después (y sólo después de hacer el test), hacemos el código necesario para que ese test pase correctamente.

Y siguiendo esta filosofía, nace precisamente TDD (Test Driven Development). Una metodología en la que primero se hace un test y luego el código necesario para que el test pase. No se hace nada de código si no falla algún test, por lo que primero debemos hacer el test que falle.

Los tres pasos de TDD

En TDD deben seguirse estos tres pasos y en este orden:

  1. Hacer un test automático de prueba, ejecutarlo y ver que falla.
  2. Hacer el código mínimo imprescindible para que el test que acabamos de escribir pase.
  3. Arreglar el código, sobre todo, para evitar cosas duplicadas en el mismo. Comprobar que los test sigue pasando correctamente después de haber arreglado el código.

Estos tres pasos deben repetirse una y otra vez hasta que la aplicación esté terminada. Vamos a ver con un poco de detalle qué implica cada uno de estos tres pasos. Por supuesto, con un ejemplo tonto. Supongamos que queremos una clase Matematicas con un método estático que hace una suma y nos devuelve el resultado.

1. Hacer el test automático.

Lo primero, hacer un test. Puede ser como esto

public void testSuma() {
   assertEquals(5, Matematicas.suma(2,3));
}

Lo primero que deberíamos ver es que el test ni siquiera compila. No existe la clase Matematicas y, por supuesto, no tiene el método suma(). Y esta es la primera ventaja de TDD. Nos ha obligado a pensar exactamente qué queremos que haga nuestra aplicación desde fuera de ella. Necesitamos una clase que tenga un método suma() con dos parámetros que nos devuelva el resultado de la suma. Este caso es muy trivial, pero cualquiera que haya programado un poco en serio, sabrá que muchas veces no sabemos exactamente qué clases hacer o qué métodos ponerle exactamente. Es más, muchas veces perdemos el tiempo haciendo métodos 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.

Además, antes de hacer la implementación, nos ha obligado a pensar el nombre de la clase y el del método, pensando sólo en cómo vamos a verlo desde fuera. Todos los que programamos sabemos que a veces elegir el nombre de las clases no es tan evidente y muchas veces, el nombre va muy ligado a cómo lo implementamos (por ejemplo, ArrayPersonas porque usamos un array, en vez de Personas a secas, o algo más bonito como la clase Muchedumbre o Pandilla).

2. Hacer el código mínimo imprescindible para que el test pase.

Para que el test pase, lo primero que hay que hacer es la clase Matematicas y ponerle el método suma(), que debe devolver algo, cualquier entero, para que al menos compile.

public class Matematicas {
   public static int suma (int a, int b) {
      return 0;
   }
}

Ya compila el test y la clase, pero si ejecutamos el test, fallará, ya que el método devuelve 0 y no 5. Corregimos la clase de la forma más inmediata posible para que pase el test. Y lo más inmediato es hacer que devuelva 5.

public class Matematicas {
   public static int suma (int a, int b) {
      return 5;
   }
}

Ya está. Todo funciona como debe. Obviamente, poner return 5 no es la solución correcta y en un caso real tan simple, pondríamos directamente return a+b. Pero de momento, déjame ponerlo así para poder explicar el siguiente paso.

3. Rehacer el código

El tercer paso es arreglar el código. Debemos arreglar sobre todo duplicidades. A veces, estas duplicidades son evidentes (por ejemplo, código repetido), pero otras, como en este caso, no son tan evidentes. ¿Qué hay repetido en este código?. Aparentemente nada, pero pensemos un poco. ¿De dónde sale ese 5?. Lo hemos puesto porque mentalmente hemos hecho la suma 2+3 y sabemos que ese es el resultado. Si no lo hubieramos hecho, habríamos puesto return 2+3. Y esa es precisamente la duplicidad. El número 2 está repetido en el código: en nuestra clase de test y en nuestra clase Matematicas. Lo mismo con el 3. Podemos eliminar esa duplicidad implementando la solución obvia:

public class Matematicas {
   public static int suma (int a, int b) {
      return a+b;
   }
}

Bueno, este caso es demasiado simple y en un caso real no haríamos tantos pasos para algo tan evidente, bastaría hacer el test y poner directamente la implementación buena. Veamos ahora algunas ideas sobre cómo llevar TDD a la práctica

¿Cómo de grandes deben ser cada iteración?

Hemos visto en el ejemplo de la suma que hemos hecho un test para la suma, hemos hecho una implementación inmediata y luego hemos refactorizado para llegar a una implementación buena. Estos pasos son excesivos para una cosa tan simple. En un proyecto real, ¿cómo de grandes son los pasos?. La solución depende de nosotros, de nuestra experiencia y de nuestra capacidad para programar.

Los test que hagamos no deben ser muy triviales, de forma que no nos eternicemos haciendo test tontos que se resuelven en cuestión de segundos. Tampoco deben ser muy complejos, de forma que un test no puede llevarnos dos días para codificarlo y otra semana más para hacer el código necesario para que pase el test y otra semana para hacer el refactor.

Debemos hacer test hasta el punto de complejidad en el que todavía nos sintamos cómodos programando y teniendo las cosas en la cabeza, pero sin llegar a ser triviales, es decir, que no nos aburramos, pero que tampoco tengamos que pasar ratos largos pensando qué hacer para resolver el test. Por eso, el límite depende de nuestra capacidad y experiencia. Prácticamente todos somos capaces de implementar un método suma() a la primera. Sin embargo, un método factorial() recursivo puede resultar demasiado para un aprendiz, aunque sea trivial para un programador con buena cabeza y varios años de experiencia. Un programador novato deberá hacer el factorial con un test para el caso trivial de factorial de 1, resolverlo, otro test para otro caso no trivial y resolverlo. El programador avanzado puede hacer el factorial() en un solo paso.

Lo ideal es que en hacer un test y el código necesario para que pase dicho test más la refactorización no se tarde más de un cuarto de hora/media hora. Una indicación clara de que estamos haciendo pasos muy grandes es que empiecen a fallarnos test inesperadamente. Una cosa es ser consciente de que hemos hecho una modificación que hace que falle un test antiguo y que lo vamos a arreglar un poco después, y otra cosa es que cuando creemos que ya hemos acabado, un test nos falle sin esperarlo.

No dejarse llevar mientras resolvemos un test

Una vez hecho el test y viendo que falla, debemos hacer el código mínimo necesario para que eso funcione. Es normal, y cualquier programador con experiencia me dará la razón, que mientras estamos codificando nos demos cuenta de posibles fallos, mejoras o necesidades en otras partes del código relacionadas con lo que estamos haciendo y que vayamos corriendo a hacerlas. Pues bien, eso es justo lo que NO debemos hacer. Debemos centrarnos en hacer que el test que hemos escrito pase lo antes posible.

Un ejemplo sencillo de esto. Imagina que hacemos un método para convertir un String a mayúsculas. Java ya tiene ese método, pero vamos a hacerlo. Nuestro test dice que si pasamos "pedro" como parámetro, el método nos debe devolver "PEDRO". Nos ponemos a codificar e inmediatamente empezamos a pensar ... "¿y si me pasan un parámetro null?. Seguro que el código rompe. Tengo que poner un if para comprobar el parámetro ...". Pues bien, eso es lo que NO hay que hacer. Codificamos el método suponiendo que el parámetro no es null y luego, más adelante, hacemos un segundo test para cuando nos pasen un parámetro null. Obviamente, esto vuelve a ser demasiado sencillo, en un caso real quizás el arreglo que creemos necesitar no tiene una solución tan rápida.

Para sentirnos cómodos dejando esas mejoras/modificaciones adicionales sin hacer, lo mejor es en cuanto las veamos, apuntar en una lista la necesidad de hacer más adelante un test para implementar esa mejora. Una vez apuntado y con la seguridad de que no se nos olvidará, podemos seguir codificando nuestro test actual. Una vez que ese test pasa y hemos hecho el refactor, podemos elegir otro de los test pendientes de la lista. En el ejemplo anterior, mientras codificamos el caso del método en el que el parámetro es correcto, se nos ocurre "¿qué pasa si me pasan un null?". Pues bien, lo apuntamos en la lista "hacer un test cuando el parámetro es null" y seguimos codificando nuestro test actual (devolver "PEDRO" cuando nos pasan "pedro").

Dejar que TDD nos lleve al diseño

El ejemplo de la suma es muy sencillo y la refactorización que hicimos para evitar duplicidades es bastante cuestionable. Veamos ahora un ejemplo algo más complejo en el que la refactorización nos lleva a un diseño más simple.

Supón que en un primer test debemos poder fijar el sueldo a un Jefe. Hacemos un test simple en el que a una clase Jefe llamamos al método setSueldo() y luego llamando a getSueldo() comprobamos que es correcto. Este primer test es bastante simple y no nos extendemos más.

Supón ahora, en un segundo test, que hacemos lo mismo con un Currito: Fijarle el sueldo. Hacemos un test similar, esta vez para la clase Currito, con su método setSueldo() y getSueldo().

Y ahora llega el momento de refactorizar y evitar duplicidades. ¿Qué tenemos duplicado? Pues dos clases Jefe y Currito que son exactamente iguales, salvo el nombre. Cualquier diseño Orientado a Objetos nos dirá que esto es correcto, que es adecuado hacer una clase para cada tipo de empledado. Pero TDD nos indica que debemos simplificar y evitar duplicidades, así que creamos una clase Empleado con métodos setSueldo() y getSueldo() y borramos las dos clases anteriores Jefe y Currito, además de rehacer los test. Ya tenemos un diseño mucho más simple (hay una sola clase y no hay código duplicado) que cumple perfectamente con ambos test.

Supón ahora un tercer test, en el que se quiere preguntar e Empleado si tiene derecho a coche de empresa y debe ser cierto sólo si es Jefe. Hacemos el test y ... ¿Volvemos a crear Jefe y Currito?. No, de momento no es necesario. Lo más simple es que empleado en el constructor admita un enumerado cuyos valores sean Jefe o Currito y se lo guarde. Luego, el método tieneCocheEmpresa() devolverá true o false en función del valor de ese enumerado.

Entonces, ¿cuándo debemos hacer las clases Jefe y Currito, heredando de Empleado o algo similar?. Pues bien. sólo cuando un test nos lo requiera y la solución más simple para ese test sea dividar la clase Empleado en dos. La condición para comprobar el enumerado es una condición que tampoco debe repetirse en el código. Si en nuestro código de la clase Empleado se empieza a repetir varias veces cosas como

if (TipoEmpleado.JEFE == tipoEmpleado) {
   ...

el evitar ese if duplicado en varios sitios empieza a invitar a pensar en otra solución, algo de herencia o quizás polimorfismo.

Foro de metodologías ágiles.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007:

Aviso Legal