February 2011

Cómo escribir tests de unidad

En mi anterior artículo os expliqué varias razones por las que los tests de unidad son importantes, pero no tuve espacio para explicaros cómo crearlos, así que he escrito éste para solucionar ese problema.


Los tests de unidad son programitas que comprueban que una “unidad” funciona correctamente. Una unidad es un grupo de funcionalidad coherente y autocontenido; esto, normalmente, significa “una clase del programa”.

Para escribir los tests de unidad, lo primero que debemos hacer es preguntarnos: “¿cuáles son las propiedades que ha de cumplir esta unidad en todo momento? ¿qué casos he de comprobar, porque los errores suelen ocultarse en ellos?”. Cuando tengamos la respuesta, sólo tendremos que escribir código que compruebe que esas propiedades realmente se cumplen, y que esos casos no producen errores. Tened en cuenta que los tests no tienen que ser exhaustivos, o nos supondrán una carga de trabajo excesiva; he escrito un artículo sobre el tema.

Para escribir tests de unidad se suelen utilizar “frameworks” que proporcionan todo el andamiaje y funciones auxiliares necesarios. Existen muchísimos frameworks libres para casi todos los lenguajes de programación: JUnit (para Java), PyUnit (para Python), JsUnit (para JavaScript), CppUnit (para C++), etc. Como los ejemplos de este artículo están escritos en Java, he decidido emplear JUnit 4.

En el mundo Java existe la convención de poner el código fuente del programa en un directorio llamado “src” y el código fuente de los tests en un directorio llamado “tests”. Además, se configura el sistema de compilación para que los ficheros .class generados al compilar los tests vayan en un directorio especial. De este modo, no se mezclan el código de producción y los tests. En Python y C++ las convenciones son diferentes, pero el objetivo es el mismo: que no se mezcle el código de producción y el código de test.

Dicho esto, vamos a ver el código de la clase para la que quiero escribir los tests de unidad. “MyList” es una implementación de una lista usando un array. Si queréis probar esto en vuestro ordenador y estáis usando Eclipse, cread un proyecto nuevo y poned el código de la clase dentro del directorio “src”.

package org.tarrio.tutorial.unittests;

public class MyList<E> {

	private static final int SIZE_DELTA = 2;

	private Object[] elems;
	private int size;

	public MyList() {
		elems = new Object[SIZE_DELTA];
		size = 0;
	}
	
	public int size() {
		return size;
	}

	public void add(E o) {
		ensureSize(size + 1);
		elems[size++] = o;
	}

	public E get(int index) {
		checkIndex(index);
		@SuppressWarnings("unchecked")
		E ret = (E) elems[index];
		return ret;
	}

	public void set(int index, E o) {
		checkIndex(index);
		elems[index] = o;
	}

	public void remove(int index) {
		checkIndex(index);
		if (index != size - 1) {
			System.arraycopy(elems, index + 1, elems, index, size - index - 1);
		}
		--size;
	}

	private void ensureSize(int newSize) {
		if (newSize > elems.length) {
			Object[] newElems = new Object[elems.length + SIZE_DELTA];
			System.arraycopy(elems, 0, newElems, 0, size);
			elems = newElems;
		}
	}

	private void checkIndex(int index) {
		if (index < 0 || index >= size) {
			throw new IndexOutOfBoundsException();
		}
	}
}

Una vez hecho esto, vamos a comenzar a escribir el test de unidad. Cread un directorio fuente (“source folder”) llamado “tests” y configuradlo para que las clases compiladas vayan a un directorio distinto de “bin” (en Eclipse, pulsad con el botón derecho sobre “tests” y seleccionad Build Path, Configure Output Folder, Specific output folder, y escribid “tests-bin”).

En este directorio fuente cread un paquete con el mismo nombre que el paquete al que pertenece la clase anterior, y dentro del paquete cread una nueva clase llamada “MyListTest”.

Ahora es cuando debéis preguntaros qué propiedades ha de cumplir MyList, para escribir tests para ellas. Por ejemplo, un MyList recién creado está vacío, al añadir un elemento a MyList el tamaño aumenta en uno, al eliminar un elemento de MyList el tamaño se reduce en uno, los elementos de MyList se pueden recuperar en el mismo orden en el que se introdujeron, etc.

Vamos a escribir un test para la primera propiedad: un MyList recién creado está vacío. Para ello, añadid el siguiente método a vuestra clase MyListTest:

	@Test
	public void newListIsEmpty() {
		MyList<Integer> list = new MyList<Integer>();
		assertEquals(0, list.size());
	}

La anotación @Test (org.junit.Test) indica a JUnit que el siguiente método es un test. El método assertEquals (org.junit.Assert.assertEquals) compara un valor esperado con un valor real, y lanza una excepción si no son iguales. Existen muchos otros métodos assertXxxx que sirven para comprobar si una condición es cierta, si dos arrays son iguales, si un objeto es null, etc.

Para ejecutar esto en Eclipse, pulsad con el botón derecho sobre el nombre de la clase y seleccionad Run As, JUnit test. Si todo va bien, deberíais ver un panel con una barra de progreso y un icono, todos de color verde. Podéis cambiar el 0 por un 1 para ver qué ocurre si la condición del “assert” no se cumple.

Ahora que sabemos cómo escribir un test de unidad, vamos a escribir los tests para “al añadir un elemento el tamaño se incrementa en una unidad” y “al eliminar un elemento el tamaño se reduce en una unidad”:

	@Test
	public void addingOneElementIncreasesSizeByOne() {
		MyList<Integer> list = new MyList<Integer>();
		assertEquals(0, list.size());
		list.add(42);
		assertEquals(1, list.size());
		list.add(592);
		assertEquals(2, list.size());
	}
	
	@Test
	public void removingOneElementIncreasesSizeByOne() {
		MyList<Integer> list = new MyList<Integer>();
		list.add(42);
		list.add(592);
		assertEquals(2, list.size());
		list.remove(0);
		assertEquals(1, list.size());
		list.remove(0);
		assertEquals(0, list.size());
	}

Habréis observado que los tres tests que hemos escrito hasta el momento comienzan de la misma forma, creando una nueva instancia de MyList. Esto es código de inicialización común a todos los tests, y sería conveniente extraerlo a un método de inicialización. JUnit nos permite definir un método que se ejecuta antes de cada test, y en el que podemos realizar todas las tareas de inicialización necesarias, marcándolo con la anotación @Before:

	private MyList<Integer> list;
	
	@Before
	public void setUp() {
		list = new MyList<Integer>();
	}
	
	@Test
	public void newListIsEmpty() {
		assertEquals(0, list.size());
	}
	
	@Test
	public void addingOneElementIncreasesSizeByOne() {
		assertEquals(0, list.size());
		list.add(42);
		assertEquals(1, list.size());
		list.add(592);
		assertEquals(2, list.size());
	}
	
	@Test
	public void removingOneElementIncreasesSizeByOne() {
		list.add(42);
		list.add(592);
		assertEquals(2, list.size());
		list.remove(0);
		assertEquals(1, list.size());
		list.remove(0);
		assertEquals(0, list.size());
	}

Como podéis ver, he eliminado la variable “list” de cada test y he creado un atributo privado “list”, que se inicializa en el método “setUp” que está marcado con @Before. Este método se ejecuta antes de cada test, y gracias a él, el atributo “list” siempre contiene una lista recién creada y vacía.

También existe una anotación @After, que sirve para ejecutar un método después de cada test. Como cada test ha de ser independiente de los demás y no debe tener efectos secundarios duraderos, se pueden utilizar los métodos @After para deshacer cualquier cambio de estado que se haya podido producir en los tests o en el método @Before.

A estas alturas, no os debería ser nada difícil escribir tests para “los elementos que se añaden a la lista se pueden recuperar en el mismo orden”, y para cualquier otra propiedad de MyList que se os ocurra. Este es vuestro primer ejercicio.

No os olvidéis de que tenéis que actualizar los tests de unidad cada vez que modifiquéis MyList. Por ejemplo, si añadís el siguiente método a MyList para añadir todos los elementos de otra lista:

	public void addAll(MyList<E> l) {
		ensureSize(size + l.size());
		System.arraycopy(l.elems, 0, elems, size, l.size);
		size += l.size;
	}

Tendréis que escribir al menos un test que compruebe que funciona bien:

	@Test
	public void addAllIncreasesSizeByOtherListsSize() throws Exception {
		MyList<Integer> someList = new MyList<Integer>();
		someList.add(1);
		someList.add(2);
		someList.add(3);
		list.addAll(someList);
		assertEquals(3, list.size());
		someList.add(4);
		list.addAll(someList);
		assertEquals(7, list.size());
	}

Lo añadimos, ejecutamos los tests, y... ¡hala! ¡El nuevo test falla! ¿Qué ha ocurrido? Resulta que tenemos un error en la clase MyList, y el test que acabamos de añadir lo ha detectado. Vuestro segundo ejercicio consiste en buscar y arreglar el error.

Vuestro tercer ejercicio consiste en cambiar la implementación de MyList, de un array a una lista enlazada. Al final del ejercicio, los mismos tests de unidad deberían seguir pasando sin necesidad de modificarlos.

Espero que este artículo os haya resultado instructivo.

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

Introducción a los tests de unidad

Tengo ganas de “evangelizar” sobre buenas prácticas de desarrollo, así que durante los próximos días publicaré una serie de artículos sobre tests de unidad. Aquí va la introducción; los demás artículos serán más técnicos. Espero que la serie os parezca interesante :)


Los tests de unidad (“unit tests” en inglés) son programitas que ponen a prueba una unidad de un programa. Una unidad puede ser una función, una clase, o incluso un módulo entero. Estos programas ejecutan diversas partes de esta unidad con diversos parámetros de entrada, estados internos, etc., y comprueban que ésta produce los resultados correctos.

Los tests de unidad se deberían escribir junto con el código al que prueban, y se deben mantener actualizados de forma que siempre pasen con éxito (ya sea arreglando los fallos que se puedan introducir en la unidad que se prueba, o actualizando el test si el funcionamiento de la unidad ha cambiado). En algunos sitios incluso escriben los tests antes de escribir el código. Lo que hacen es codificar los requisitos en los tests, y así, cuando todos los tests pasan con éxito, saben que el módulo cumple todos los requisitos y además funciona bien.

Podría parecer a simple vista que mantener los tests de unidad al mismo tiempo que el código “de verdad” cuesta más trabajo que, simplemente, no tener tests de unidad. Sin embargo, los tests de unidad proporcionan varias ventajas que compensan con creces su existencia.

La principal es la mayor velocidad de desarrollo. Cuando hacéis un cambio en un programa y no tenéis tests de unidad, la única forma de probarlo consiste en compilar el programa, ejecutarlo, ir hasta la parte que habéis cambiado, hacer la prueba, etc. Sin embargo, con tests de unidad basta con compilarlos y ejecutarlos, y en menos de un minuto tenéis el resultado.

Además, los tests de unidad evitarán que introduzcáis muchos errores, ya que los tests no pasarán con éxito hasta que la unidad a prueba funcione razonablemente bien. Y, si en el futuro descubrís algún error, sólo tenéis que añadir un test que capture ese error, arreglarlo para que el test pase con éxito, y sabréis que no volveréis a introducir ese error en el futuro.

Otra ventaja es que con los tests de unidad podéis estar seguros de que vuestros cambios no tendrán efectos imprevistos. Por ejemplo, algo que se hace a menudo es cambiar la implementación de un módulo sin modificar su interfaz. Si tenéis tests de unidad e introducís algún error en la nueva implementación, alguno de estos tests fallará; cuando todos los tests pasen con éxito, podéis estar razonablemente seguros de que la nueva implementación es correcta. Sin tests de unidad no podéis estar tan seguros.

Otra ventaja importante es que los tests forman parte de la documentación del software. Los Javadoc tienen la molesta costumbre de quedar obsoletos. Tal vez hoy una función de búsqueda devuelve “null” al buscar un elemento que no existe, pero si mañana alguien la cambia para lanzar una excepción y no actualiza el Javadoc, el compilador no protestará y el Javadoc quedará obsoleto. Sin embargo, si hay un test que comprueba que, al pasarle un elemento inexistente, la función devuelve “null”, cuando esta persona haga el cambio el test fallará, así que tendrá que actualizar el test (o dejar la función como estaba). Por tanto, los tests de unidad son una documentación que nunca queda obsoleta.

Por supuesto, los tests de unidad no sirven de nada si nadie les presta atención. En muchas organizaciones tienen políticas que obligan a ejecutar los tests de unidad antes de hacer “commit” (por supuesto, los tests tienen que pasar con éxito). En algunos sitios tienen “compiladores continuos” (“continuous build”), que son máquinas que toman la última versión del software, lo compilan, ejecutan los tests de unidad y avisan si alguno falla. En otros sitios integran los tests de unidad en el sistema de control de versiones, de forma que no se puede hacer “commit” si algún test falla.

En posteriores historias explicaré cómo escribir tests de unidad, cómo usar inversión de dependencias, mocks y objetos falsos para aislar la unidad que queremos probar, y cómo hacer desarrollo dirigido por los tests.

(Siguiente artículo).