Aplicando el patrón builder para escribir tests unitarios

Hace ya un tiempo terminé una de mis series favoritas de posts sobre antipatrones al escribir tests unitarios, y una de las cosas que decía que había que evitar era depender de APIs no testeadas. La solución que proponía se basaba en el uso del patrón builder pero no profundizaba mucho en cómo implementarlo realmente.

En este post vamos a ver cómo podemos implementar un builder que nos ayude a conseguir dos cosas:

  1. Escribir tests más legibles, haciendo que sea más fácil entender de qué forma estamos configurando los objetos sobre los que vamos a actuar.
  2. Desacoplar los tests del API concreta que necesitamos para configurar esos objetos.

Este tipo de cosas siempre se ven mejor con un ejemplo, así que vamos a preparar un pequeño modelo sobre el que queremos escribir los tests.

El modelo

Para que sea algo familiar familiar al todo el mundo, usaremos el típico ejemplo de gestión de pedidos, con clientes, direcciones, productos, pedidos y líneas de pedido:

Modelo de ejemplo

Con el código fuente que podéis encontrar aquí es fácil ver que la idea es tener un modelo de dominio no-anémico. Los objetos no son meros sacos de propiedades, sino que queremos que las operaciones estén bien encapsuladas y las clases se aseguren de hacer cumplir sus invariantes, por lo que la creación de objetos debe hacerse con cuidado para asegurarse de que todo es coherente.

El test

Partiendo de ese modelo, vamos a escribir un posible test (también muy típico):

  • Los clientes preferentes tienen un 10% de descuento.

La forma directa de escribir ese test usando el modelo descrito anteriormente sería algo así:

[Test]
public void Preferred_Customers_Should_Get_A_10_Percent_Discount()
{
  var address = new Address("C/ Viriato, 22", "Leganes", "Madrid", "28047");

  var customer = new Customer(Guid.New(), "Lucas Grijander", address);
  customer.MakePreferred();

  var whitePaint = new Product("PINT-001-WHT", "Pintura Blanca");
  var blackPaint = new Product("PINT-002-BLK", "Pintura Negra");

  var order = new Order(100, customer, new DateTime(2014, 1, 19));
  order.AddLine(whitePaint, 1m, 10m);
  order.AddLine(blackPaint, 1m, 20m);

  Assert.That(order.Total, Is.EqualTo(27m));
}

No es que sea un test tan terrible, pero pese a ser un modelo bastante sencillo, ya podemos ver varios problemas:

  1. Hay muchas líneas de código para preparar el test y es incómodo de leer.
  2. No es fácil ver que la parte importante de la preparación es convertir el cliente en preferred con la llamada a customer.MakePreferred()
  3. Hemos introducido dependencias sobre los constructores de Address, Customer, Product y Order, así como sobre los métodos para hacer un cliente preferido y para añadir líneas a un pedido, cuando en realidad lo único que nos interesa es tener un pedido asociado a un cliente preferido y verificar que el total se calcula correctamente.

    Estas dependencias, que pueden no parecer tan graves por tratarse de un sólo test, se convierten en un verdadero problema cuando tienes cientos de tests, necesitas cambiar un API concreta y acabas teniendo que tocar un montón de tests que a los que, en principio, debería darles igual ese API.

Los builders

Para solucionar los problemas anteriores, me gustaría poder escribir el test así:

[Test]
public void Preferred_Customers_Should_Get_A_10_Percent_Discount()
{
  Customer customer = Build.Customer("Lucas Grijander")
                           .Preferred();

  Order order = Build.Order(customer)
                     .AddOne("Pintura Blanca", 10m)
                     .AddOne("Pintura Negra", 20m);

  Assert.That(order.Total, Is.EqualTo(27m));
}

El número de líneas de código se ha reducido bastante, pero lo más importante es que hemos quitado mucho ruido. Ahora se ve mucho más claro que lo que nos importa del cliente es que es Preferred, y no su dirección o su código de cliente. Queda también claro que no nos importa el número de pedido, ni la fecha en que se realiza, ni si los productos tiene una referencia u otra. Al tener menos cosas de las que estar pendiente, el test se hace mucho más fácil de entender.

Además, ya no dependemos de las APIs concretas de las clases que necesitamos en los tests, sino que tenemos una capa de indirección a través del Builder que nos protege de los cambios en las APIs de los objetos reales.

Podéis encontrar la implementación completa del builder del ejemplo aquí pero vamos a comentar algunas partes:

public static class Build
{
  public static OrderBuilder Order(Cutomer customer)
  {
    return new OrderBuilder(customer);
  }
}

public static class OrderBuilder
{
  private readonly Order order;

  public OrderBuilder(Customer customer)
  {
    order = new Order(1000, customer, DateTime.Today);
  }

  public OrderBuilder AddOne(string productName, decimal price)
  {
    Product product = Build.Product(productName);
    order.AddLine(product, 1, price);
    return this;
  }

  public static implicit operator Order(OrderBuilder builder)
  {
    return builder.order;
  }
}

En los builders podemos ir añadiendo aquellos métodos (y sobrecargas) que necesitemos para configurar los objetos de forma adecuada para nuestros tests, y aprovechando el operador de conversión implícita podemos realizar la construcción final del objeto. Generalmente, suelo utilizar una clase Build como fachada estática para acceder a los diferentes tipos de builders que tengo en la aplicación.

Conclusiones

El uso de builders para preparar los datos que usamos en los tests ayuda mucho a mejorar la legibilidad de los tests y a independizarlos de las APIs que no son importantes para el propio test.

Además de utilizarlos en tests de estado como el del ejemplo, suelo también emplearlos en tests de integración contra la base de datos cuando necesito generar distintos tipos de datos para, por ejemplo, comprobar el correcto funcionamiento de las consultas usadas para obtener informes.

Hay que tener en cuenta que crear estos builders requiere código que hay que escribir y mantener, por lo que no siempre compensa escribirlos y es bueno esperar un poco antes de empezar a crear builders para todo.

Un comentario en “Aplicando el patrón builder para escribir tests unitarios

  1. Pingback: Enlaces semanales 2014 #05 | Dev attitude

Comentarios cerrados.