Imaginemos el siguiente escenario: estamos codificando el test para guardar un cliente, y necesitamos que ya exista en la base de datos la empresa a la cual pertenence. ¿Qué hacemos? Una primera solución rápida es contar con datos de tests ya existentes en la base de datos, y confiar en que dichos datos sirvan para nuestro test (o, si no alcanzan, agregar los datos de tests necesarios a la base). Y sin embargo... hacer esto es el comienzo de graves problemas.
El antipatrón: "Invitado misterioso"
Le damos la bienvenida al Invitado Misterioso (o "mistery guest" en inglés), un antipatrón de diseño de test. La situación: nuestros tests dependen de datos externos (como información en una base de datos) para funcionar. La solución aparente: mover la creación de dichos datos fuera del test en cuestión (por ejemplo, a un schema.sql). El problema: ahora el test necesita de un "entorno misterioso" (el cuál no se ve directamente en el mismo test) para funcionar. Peor aún, el test no hace evidente la relación causa-consecuencia entre los datos de ingreso (que ahora están ocultos) y la salida esperada.
Pero los problemas no terminan aquí: si mantenemos un schema.sql único para todos los tests, este va a crecer con el tiempo, empeorando la situación (cada vez más datos, para distintos tests, sin poder determinar de manera simple qué dato necesita cuál test). Por otro lado, si decidimos mantener scripts sql diferentes para cada test, la situación tampoco es mejor: el mantenimiento de dichos archivos se vuelve un problema con el tiempo, especialmente cuando se necesita realizar algún cambio a los datos o al esquema en general.
Teniendo esto en claro, la solución es evidente: cada test debería preparar los datos mínimos que necesita para su funcionamiento (haciendo explícita la relación causa-consecuencia). Claro que esto trae sus propios problemas: ¿cómo crear estos datos, tests tras test, sin que termine resultando algo engorroso de mantener o de leer?
El patrón de diseño "Builder"
Permítanme presentarle a los Builder: un patrón de diseño orientado a solucionar la creación de objetos, que resulta muy útil para encarar este problema. Básicamente, un Builder es una clase que se encarga de construir instancias de otro objeto, personalizando su creación. La idea es contar con Builders para nuestros objetos de dominio (aquellos que persisten, por ejemplo), y construirlos de manera semántica, con sentido de negocio. No sólo construirlos, sino también persistirlos.
Volvamos a nuestro test para guardar un cliente, implementado con un builder. Tengamos en cuenta que la base de datos de test está vacia, y crearemos todos los datos necesarios en el mismo test.
@Test
public void guardar_conDatosBasicosCompletos_guardaCliente() {
Empresa empresa = EmpresaBuilder.habilitada().build(entityManager);
Cliente cliente = ClienteBuilder.paraEmpresa(empresa).build();
clienteService.guardar(cliente);
assertNotNull(cliente.getId());
assertNotNull(cliente.getFechaCreacion());
}
Nótese el uso de EmpresaBuilder y ClienteBuilder para construir instancias de Empresa y Cliente, usando un API que tenga sentido de negocio. Es decir, en vez de crear una instancia de Cliente y setearle la empresa y demás datos válidos que necesita, creamos "un cliente para la empresa X", y el Builder se encargará de darnos una instancia válida de un Cliente, con los datos mínimos necesarios.
Por otro lado, EmpresaBuilder recibe al entityManager en su método build(), lo cual además de construir la instancia de la Empresa la persiste en la base de datos. De esta manera, podemos simular los datos precisos y necesarios para la ejecución de nuestro test. Partimos un esquema de base de datos vacio, y cada test lo completa con la información necesaria exclusiva para la corrida.
El código del Builder
El código del Builder es relativamente sencillo. Para simplificarlo, utilizamos la librería test-utils de SomosPNT. Esta librería provee utilidades para la persistencia y un marcco general para construir Builders orientados al test, pero bien pueden construirse manualmente. No debemos olvidar que lo importante del Buidler es su API orientada al negocio!
Nuestro EmpresaBuidler entonces:
package com.dosideas.builder;
import com.somospnt.test.builder.AbstractPersistenceBuilder;
public class EmpresaBuilder extends AbstractPersistenceBuilder<Empresa> {
private EmpresaBuilder() {
instance = new Empresa();
}
public static CategoriaBuilder habilitada() {
EmpresaBuilder builder = new EmpresaBuilder();
builder.instance.setHabilitada(true);
builder.instance.setNombre("Empresa de Test");
builder.instance.setDescripcion("Una empresa habilitada para testing");
return builder;
}
}
Para destacar de EmpresaBuidler, nótese que el método habilitada() setea los datos mínimos para crear una empresa válida y habilitada. Al extender de AbstractPersitenceBuilder ya tenemos un método build() que nos devuelve la instancia lista para usar.
Y ClienteBuilder:
public class ClienteBuilder extends AbstractPersistenceBuilder<Cliente> {
private ClienteBuilder() {
instance = new Cliente();
}
public static CategoriaBuilder paraEmpresa(Empresa empresa) {
ClienteBuilder builder = new ClienteBuilder();
builder.instance.setEmpresa(empresa);
builder.instance.setNombre("Invasor Zim");
builder.instance.setFechaNacimiento(LocalDate.now().minusYears(50));
return builder;
}
}
Para destacar de ClienteBuilder, nótese que el método paraEmpresa() no sólo setea la empresa al cliente, sino que le completa datos mínimos necesarios (como ser su fecha de nacimiento). Igual que con EmpresaBuidler, al heredar de AbstractPersitenceBuilder obtenemos el método build(entityManager) que guardará la instancia en la base de datos antes de retornala.
Mini-reglas para los Builder
Con el uso, encontramos útil establecer las siguientes reglas para la construcción de los Builder, orientadas a facilitar su construcción, uso y mantenimiento:
- Un Builder siempre devuelve un objeto con un estado válido. Si se necesita una instancia con un estado inválido (por ejemplo, un Cliente con un email incorrecto), será el test el encargado de invalidarlo.
- Un Builder puede invocar a otro Builder para completar datos. Por ejemplo, ClienteBuilder podría invocar a DomicilioBuilder para crear un Domicilio y agregárselo al Cliente.
- Un Builder puede persitir el objeto en la base de datos, a través del métod build(entityManager). De la misma forma, este método puede llamar a otros Builder relacionados para persistir objetos necesarios.
- El API del Builder siempre es semántico para el negocio. Por ejemplo, en vez de ClienteBuilder.setDomicilio(null) preferimos ClienteBuilder.sinDomicilio().