Desarrollo dirigido por los tests

Como su propio nombre indica, el desarrollo dirigido por los tests (“test-driven development”, o TDD en siglas) consiste en escribir primero los tests de unidad y luego escribir el código que hace que estos tests pasen con éxito.

Esta técnica tiene varias ventajas. Por ejemplo, el código escrito de esta manera suele exponer interfaces más limpios, fáciles de usar y desacoplados de la implementación que el código escrito de forma normal. Además, el código suele tener menos errores, y menos funcionalidades añadidas de forma especulativa.

Permitidme explicar este último punto, ya que es menos obvio que los anteriores. Muchas veces, cuando escribimos código, solemos añadir cosas que no necesitamos inmediatamente, pero que suponemos que vendrán bien en el futuro, cuando tengamos que hacer escalar la aplicación o cuando tengamos que añadir nuevas funciones o cuando tengamos que sustituir la base de datos. El problema con este código extra es que... bueno, es código extra: más código que hemos de mantener, más sitios donde pueden esconderse bugs, más espacio que ocupa el programa, más despacio que el programa va, etc. Por lo tanto, es recomendable evitar añadir código de forma especulativa y dejarlo para cuando realmente vayamos a necesitarlo; por desgracia, es una tentación muy difícil de evitar. Sin embargo, al hacer TDD es más fácil concentrarse en escribir sólo el código que hace que los tests pasen ahora mismo y no caer en la tentación.

Por supuesto, el TDD tiene inconvenientes además de ventajas. Por ejemplo, en ocasiones es muy difícil escribir tests para un código todavía inexistente. Otro inconveniente que podéis encontraros es que alguna gente se pasa de lista y cae en la tentación de escribir código que sólo funciona para los tests, en lugar de escribir código que pasa los tests porque es correcto. Si os encontráis con uno de estos, tenéis mi permiso para darles una bofetada (pero no digáis que fui yo quien os lo dio).

En algunos sitios llevan esta técnica un poco más lejos y hacen que una persona escriba los tests y luego otra persona distinta escriba el código. De este modo consiguen que dos personas conozcan el código en lugar de una sola y hacen que sea más difícil introducir bugs (es más difícil que dos personas introduzcan errores que se neutralizan mutuamente que que lo haga una persona sola).

Vamos a ver un pequeño ejemplo de TDD, escribiendo una pequeña implementación de un conjunto en Java. Vamos a comenzar creando la clase MySetTest, donde escribiremos los tests de unidad. También creamos la clase MySet, pero sólo pondremos en ella el esqueleto; sólo lo necesario para que Eclipse no se queje y para poder compilar y ejecutar los tests.

public class MySetTest {
	private MySet<Object> set;
	
	@Before
	public void setUp() {
		set = new MySet<Object>();
	}
	
	@Test
	public void newSetIsEmpty() throws Exception {
		assertEquals(0, set.size());
	}
}

public class MySet<E> {
	public int size() {
		return 0;
	}
}

Como podéis ver, he escrito ya un test de unidad para comprobar que un nuevo conjunto está vacío, y he añadido en MySet el correspondiente esqueleto para el método size(). Vamos a añadir unos pocos tests más:

public class MySetTest {
	private static final Object OBJ1 = new Object();
	private static final Object OBJ2 = new Object();
	
	private MySet<Object> set;
	
	@Before
	public void setUp() {
		set = new MySet<Object>();
	}
	
	@Test
	public void newSetIsEmpty() throws Exception {
		assertEquals(0, set.size());
	}
	
	@Test
	public void addElementIncreasesSizeIfElementIsNew() throws Exception {
		set.add(OBJ1);
		assertEquals(1, set.size());
		set.add(OBJ1);
		assertEquals(1, set.size());
		set.add(OBJ2);
		assertEquals(2, set.size());
	}

	@Test
	public void onlyContainsAddedElements() throws Exception {
		assertFalse(set.contains(OBJ1));
		assertFalse(set.contains(OBJ2));
		set.add(OBJ1);
		assertTrue(set.contains(OBJ1));
		assertFalse(set.contains(OBJ2));
	}
}

public class MySet<E> {
	public int size() {
		return 0;
	}

	public void add(E e) {
	}

	public boolean contains(E e) {
		return false;
	}
}

Nuevamente, fijaos en que mi implementación de MySet sólo contiene lo necesario para que los tests compilen y Eclipse no me llene la pantalla de líneas rojas.

Ahora podemos ejecutar los tests, y veremos que algunos pasarán y otros (la mayoría) fallarán. Nuestra tarea ahora consiste en rellenar el esqueleto de MySet con el código necesario para hacer que los tests pasen. Por ejemplo:

public class MySet<E> {
	private List<E> elems;
	
	public MySet() {
		elems = new ArrayList<E>();
	}
	
	public int size() {
		return elems.size();
	}

	public void add(E e) {
		if (!elems.contains(e)) {
			elems.add(e);
		}
	}

	public boolean contains(E e) {
		return elems.contains(e);
	}
}

Ahora, con sólo ejecutar los tests, sabemos que este código funciona correctamente. Por supuesto, este ejemplo es muy simple, pero imaginad las ventajas que os proporcionaría a la hora de escribir un código más complicado.

Como ejercicio, podéis probar a escribir la función para eliminar un elemento del conjunto: primero escribid un test que compruebe que al eliminar un elemento, el tamaño del conjunto disminuye en 1 si éste estaba en el conjunto, y otro que compruebe que un conjunto no contiene un elemento eliminado. Después, escribid el código que haga que los tests pasen.

La técnica del TDD también es muy útil a la hora de corregir bugs. Para aplicarla, primero escribís un test que falle si el bug existe, y luego modificáis el código para que pase este test. Al hacerlo así, os lleváis dos cosas de regalito: sabéis que vuestro arreglo no afecta al resto de funcionalidades, y sabéis que este bug no reaparecerá en el futuro, ya que ahora tenéis un test que lo detecta.

Hasta ahora hemos visto cómo escribir tests para unidades más bien simples. Por desgracia, casi siempre tenemos que tratar con componentes que dependen de otros componentes, y otras cosas que hacen que escribir tests de unidad pueda ser muy engorroso. En las próximas entregas os explicaré cómo organizar vuestro código para aislar estas dependencias y facilitar la escritura de los tests de unidad.

(Primer artículo, siguiente artículo).