La unidad de un test unitario no es una clase

En alguna ocasión he hablado sobre test unitarios y la forma en que los suelo implementar. Últimamente he notado que cada vez los tests que más me gustan son menos unitarios en el sentido más tradicional de la palabra y más de integración, no tanto por el acceso a sistemas externos sino porque testeo varias clases a la vez.

Al final, todo se reduce a decidir cuál es el límite de una unidad testeable con un test unitario. Esto, que puede parece una mera discusión semántica (¿qué importa cómo catalogue el test? lo importante es lo que testea), en realidad acaba afectando bastante a la utilidad de los tests que estamos escribiendo.

Cuando utilizar un lenguaje puramente orientado a objetos, como por ejemplo C# o Java, parece que lógico pensar que la unidad a testear debe ser la clase. Cuando se aplica esto de manera estricta, cada clase con visibilidad pública acaba teniendo su(s) clase(s) de test asociada(s) y, generalmente, las dependencias de esa clase se acaban sustituyendo por mocks, fakes o stubs. Sin embargo, esto no siempre proporciona los mejores resultados.

Ante todo, debemos tener claro lo que pretendemos conseguir con los tests:

  • Si estamos usando TDD, los tests nos ayudan a guiar el desarrollo, indicándonos qué debemos implementar en cada momento.
  • Independientemente de escribir los tests antes o después del código «real», permiten validar la funcionalidad implementada.
  • Actúan como red de seguridad al refactorizar el código.
  • Proporcionan protección frente a bugs de regresión.

El problema de tomar la clase como unidad a testear puede convertir los tests unitarios en frágiles a la hora de cumplir con dos de esos objetivos: validar la funcionalidad y ayudar durante la refactorización.

Si mantenemos una estructura muy rígida en la cual cada clase debe ser testeada por separado e independizada de la implementación de sus dependencias, cuando queramos refactorizar una clase en un conjunto de clases más pequeñas, deberíamos testear cada nueva clase por separado y cambiar los tests originales para que no dependan de la implementación de sus dependencias.

Veamos un ejemplo sencillo. Supongamos que tenemos una clase que representa un pedido en la cual ciertas acciones dependen del estado en que se encuentra el pedido. Así, un pedido sólo puede enviarse al cliente si está pendiente de envío, y sólo puede facturarse una vez enviado. Tendríamos algo así:

public class Order
{
	private enum OrderStatus
	{
		Pending,
		Shipped,
		Invoiced,
	}
	
	private OrderStatus status = OrderStatus.Pending;

	public void Ship()
	{
		if (status != OrderStatus.Pending)
			throw new InvalidOperationException("Only pending orders can be shipped");
		
		// Do whatever is required to ship the order
		
		status = OrderStatus.Shipped;
	}
	
	public void Invoice()
	{
		if (status != OrderStatus.Shipped)
			throw new InvalidOperationException("Only shipped orders can be invoiced");
		
		// Do whatever is required to invoice the order

		status = OrderStatus.Shipped;
	}
}

[TestFixture]
public class OrderTest
{
	[Test]
	public void A_Pending_Order_Can_Be_Shipped()
	{
		var order = new Order();
		order.Ship();
	}
	
	[Test]
	public vodi A_Pending_Order_Cannot_Be_Invoiced()
	{
		var order = new Order();
		Assert.Throws<InvalidOperationException>(() => order.Invoice())
	}
	// ... more tests
}

Nada especial. Típica implementación sencilla con sus tests asociados. Si en algún momento esta clase empieza a crecer, puede ser razonable refactorizar a un patrón estado. De esa forma, el código cambiaría a algo así:

public interface IOrderState
{
	void Ship();
	void Invoice();
}

public class PendingOrderState : IOrderState
{
	private Order order;
	
	public PendingOrderState(Order order)	
	{
		this.order = order;
	}

	public void Ship()
	{
		// ... do whatever is required to ship an order
		
		order.State = new ShippedOrderState(order);
	}
}

// ... other order states...

public class Order
{
	private OrderState state = new PendingOrderState(this);
	
	internal OrderState State { get; set; }

	public void Ship()
	{
		state.Ship();
	}
	
	public void Invoice()
	{
		state.Ship();
	}
}

En teoría, si seguimos considerando la clase como la unidad a testear, deberíamos escribir tests unitarios para cada una de las clases que implementan estados de Order y cambiar los tests de Order para usar un stub/fake/mock/lo-que-más-te-guste de IOrderState y evitar así que los tests dependa de implementaciones concretas de IOrderState. Eso sería un error. En realidad, los tests que ya tenemos nos sirven perfectamente tal y como están, aunque estén testeando varias clases a la vez.

Lo que nos interesa de los tests es que sigan funcionando cuando refactoricemos algo, y que cuando se pase un test, tengamos cierto grado de confianza en que las cosas realmente funcionan. Si dentro de dos semanas queremos cambiar el patrón estado por un patrón estrategia, o decidimos que nos gustaba más la implementación original, deberíamos poder hacerlo sin tocar ningún test.

En un caso como este, la unidad de testeo no sería la clase, sino un grupo de clases que ofrecen una funcionalidad concreta. A falta de un nombre mejor, podemos llamar a esto un cluster de clases. Un cluster de clases es un grupo de clases con una alta cohesión que funcionan como una sola unidad funcional. Son clases que no tienen sentido por si mismas fuera del grupo al que pertenecen.

Hay veces que merece la pena implementar este cluster de clases de una forma que realmente indique a nivel de código que se trata de un grupo de clases único y unido. Para eso, una buena herramienta es utilizar clases privadas. Usando esa técnica, en el ejemplo anterior tanto IOrderState como sus distintas implementaciones serían clases privadas internas a Order, con lo que estamos dejando claro que sólo se usan en relación a Order y forman parte de una misma unidad funcional. En C# además se puede sacar partido de las clases partial para separar la implementación en distintos ficheros (uno por cada clase interna) y mantener el código claro y limpio.

Nada es perfecto, y al aplicar esta idea hay que tener cuidado. Hay veces que una clase empieza siendo parte de un cluster, pero llega un momento en que resulta de utilidad en otros puntos de la aplicación. Cuando pasa eso, es necesario ser consciente de ello, «ascender» a la clase de categoría (formaría parte de su propio cluster) y escribir para ella sus propios tests independientes de quién la esté usando.

3 comentarios en “La unidad de un test unitario no es una clase

  1. Pingback: Cómo NO escribir test unitarios: Replicando cálculos en el test

  2. Pingback: Los test de integración y el contenedor de IoC | Blog de Nicoloco

  3. Tengo que confesarte que he empezado leyendo el artículo con algo de miedo. Pero das en el clavo cuando hablas del «clúster de clases».

    Muchas veces, querer probar por separado clases como las del ejemplo que pones es rizar el rizo. Una prueba unitaria debe probar una «unidad de código», requerimiento o funcionalidad, que como bien dices puede ir más allá de una sola clase.

Comentarios cerrados.