Cómo utilizo Patrones de Diseño en Test Unitarios

Existen patrones de diseño que nos van a servir para mejorar la calidad del código de nuestros tests.

Patrones como Creation Method, Object Mother o Builder nos van a ayudar a mejorar la legibilidad y nos van a permitir también reutilizar código entre nuestros tests.

De lo simple a lo complejo

Suelo tener una norma que me funciona bastante bien que es ir de lo simple a lo complejo.

No suelo empezar a crear tests utilizando muchos patrones como los que vamos a ver en este artículo, intento empezar por la opción más simple.

A medida que voy creando tests y voy identificando código duplicado, acoplado o que los test se empiezan a leer mal es cuando empiezo a optimizar el código a base de refactorizaciones.

Opción simple

Como ya escribí en el artículo Escribiendo test legibles, me gusta que los test se mantengan pequeños porque a medida que son mas grandes se empiezan a leer peor y son más difíciles de mantener.

Si el objeto a probar (SUT) es una entidad de dominio pequeña, donde el constructor para crear instancias no tiene muchos parámetros o donde no tenemos que interactuar con el objeto una vez creado para situarlo en un estado en particular, es bastante sencillo mantener esta simplicidad.

public class CountryShould {
    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void throw_exception_if_code_is_not_provided (){
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("name is required");

        new Country("es","");
    }
}

Object mother

No todos los test son el ejemplo sencillo, existen situaciones donde es necesario escribir código adicional para configurar el fixture o contexto de la prueba, por ejemplo:

  • Cuando necesitamos hacer test de un objeto que tienen colaboradores.
  • Si tengo que interactuar con el objeto a probar para situarlo en un estado que me interesa para una prueba concreta.
  • Cuando se hacen pruebas de aceptación sobre la interfaz de usuario y nos interesa simular la capa de datos usando mocks, stubs ...

En este momento suelo escribir el test primero.

public class MoneyShould {
    @Test
    public void return_display_name_as_amount_concat_with_currency_symbol() {
        Currency EUR = Currency.getInstance("EUR");

        Money money = new Money(new BigDecimal("100.00"), EUR);

        assertThat(money.getDisplayName(), is("100.00 €"));
    }
}

El primer problema que suele aparecer cuando existen colaboradores es que se pierde legibilidad en el test, porque la forma en la que se crea el colaborador es indiferente para el test del objeto a probar (SUT).

Adicionalmente aparece otro problema, a medida que se crean más tests vamos a tener que crear el colaborador muchas veces, esto provoca un uso masivo de constructores o métodos de factoría del propio colaborador.

Cuando creamos manualmente colaboradores nuestros test se vuelven frágiles, al igual que cuando utilizamos frameworks directamente en los test.

Cuando cambian los constructores o métodos de factoría tenemos que ir por todos esos sitios actualizando la creación de objetos para adaptarlo.

Una forma de minimizar este problema es utilizar el patrón ObjectMother.

Normalmente utilizo este patrón cuando de un objeto existe un juego de combinaciones concretas que suelo utilizar en gran número de tests.

Otro tipo de objetos con lo que suele venir muy bien utilizar el patrón Object Mother es con los típicos maestros.

Refactorizando el ejemplo anterior nos quedaría así:

public class Currencies{
    public static Currency EURO() {
        return Currency.getInstance("EUR");
    }

    public static Currency DOLLAR() {
        return Currency.getInstance("USD");
    }
}

public class MoneyShould {
    @Test
    public void return_display_name_as_amount_concat_with_currency_symbol() {
        Money money = new Money(new BigDecimal("100.00"), Currencies.EURO());

        assertThat(money.getDisplayName(), is("100.00 €"));
    }
}

Patrón builder

Pero hay situaciones donde el patrón Object Mother se queda corto:

  • Si hay varios objetos a crear, la legibilidad del test aunque mejora, todavía hay configuración del fixture que hace que la legibilidad del test este lejos de ser la mejor.
  • Si necesitamos muchos escenarios distintos de un mismo objeto, podríamos utilizar el patrón Object Mother pero tendríamos que crearnos un método por cada escenario dentro del mismo objeto. Esto provocaría tener un objeto demasiado grande, muy acoplado a los tests y difícil de mantener.
  • Hay valores dentro de la creación del objeto que son irrelevantes para el test y pueden asignarse unos valores por defecto cualquiera

Cuando se produce alguna de estas situaciones es cuando suelo utilizar el Patrón Builder.

Este patrón te permite tener mucha más flexibilidad para crear el fixture y el objeto que usamos esta menos acoplado y mejora la legibilidad porque estamos ocultando como se crea el objeto realmente.

El Patrón Builder se suele implementar con api fluida y mejora la legibilidad donde se esta utilizando.

public class MoneyBuilder {

    private String amount;
    private Currency currency;

    public MoneyBuilder withAmount(String amount){
        this.amount = amount;
        return this;
    }

    public MoneyBuilder withCurrency(Currency currency){
        this.currency = currency;
        return this;
    }

    public Money build(){
        return new Money(new BigDecimal(amount),currency);
    }
}

public class MoneyShould {
    @Test
    public void return_display_name_as_amount_concat_with_currency_symbol() {
        
        Money money = new MoneyBuilder()
                .withAmount("100.00")
                .withCurrency(Currencies.EURO())
                .build();

        assertThat(money.getDisplayName(), is("100.00 €"));
    }
}

Creation Method

Es posible que si para configurar el fixture hay que crear más objetos que en los casos anteriores, van a seguir apareciendo los mismos problemas que teníamos antes aunque en menor medida:

legibilidad del test, configuración de colaboradores irrelevante para el test actual , etc..

Vamos a ver un caso donde combinamos un objeto producto con los que ya teníamos:

public class ProductBuilder {

    private String title = "Product example";
    private String amount ="";
    private Currency currency = Currencies.EURO();

    public ProductBuilder withTitle(String title){
        this.title = title;
        return this;
    }

    public ProductBuilder withAmount(String amount){
        this.amount = amount;
        return this;
    }

    public ProductBuilder withCurrency(Currency currency){
        this.currency = currency;
        return this;
    }

    public Product build(){
        Money price = new MoneyBuilder()
                .withAmount(amount)
                .withCurrency(Currencies.EURO())
                .build();

        Product product = new Product(title);

        if (amount!= null && !amount.isEmpty())
            product.ChangePrice(price);

        return product;
    }
}


public class ProductShould {

    @Test
    public void is_on_sale_if_its_not_free_and_is_marked_as_on_sale() throws FreeProductOnSaleException {

        Product product = new ProductBuilder()
                .withAmount("100.00")
                .withCurrency(Currencies.EURO())
                .build();

        product.markOnSale();

        assertThat(product.isOnSale(), is(true));
    }
}

En este test la cantidad y la moneda del precio es irrelevante para el test, se esta perdiendo legibilidad.

En este caso suelo utilizar un método de creación dentro de la propia clase de test que libere completamente al método del test de conocer la configuración del fixture.

Este método de creación lo suelo crear con el prefijo given.

public class ProductShould {

    @Test
    public void is_on_sale_if_its_not_free_and_is_marked_as_on_sale() throws FreeProductOnSaleException {

        Product product = givenANonFreeProduct();

        product.markOnSale();

        assertThat(product.isOnSale(), is(true));
    }

    private Product givenANonFreeProduct() {
        return new ProductBuilder()
                    .withAmount("100.00")
                    .withCurrency(Currencies.EURO())
                    .build();
    }
}

Curso y artículos relacionados

Conclusiones

En este artículo hemos visto los patrones que suelo utilizar para optimizar el código de los test cuando empiezo a identificar código duplicado, demasiado uso de constructores de las entidades de dominio o si se empieza a perder legibilidad en los test.

No hay que ponerse la venda antes que la herida y solo cuando empezamos a ver problemas en el código es cuando debemos empezar a optimizar el código.