Cómo NO escribir tests unitarios: Dependiendo de APIs no testeadas

En el anterior post de la serie de cosas que aprendí por las malas mientras escribía tests explicaba los problemas de depender en exceso de los constructores de otas clases y proponía como alternativa el uso del patrón ObjectMother.

Se puede producir una dependencia similar cuando en lugar de (o además de) depender del constructor, existe una dependencia del API de objeto para poder ponerlo en un estado determinado.

Por ejemplo, podemos tener un caso como el siguiente el que queremos comprobar que un objeto Task, cuando ha sido completado, no puede ser asignado a ningún usuario:

[Test]
public void A_Completed_Task_Cannot_Be_Reassigned()
{
    var task = new Task("Write Post");
    task.Assign(ObjectMother.Lucas);
    task.Completed();

    Assert.Throws<InvalidOperationException>(() => task.Assign(ObjectMother.Marcos));
}

Para escribir este test lo único que nos importa del objeto Task es que esté completado, pero lo que haya que hacer para completarlo nos da igual. Sin embargo, estamos acoplando el test al API concreta usada para completar un Task, por lo que cuando ese API cambie, será necesario modificar el test, aunque realmente lo que estamos testeando no ha cambiado (y aquí las herramientas como Resharper no te pueden ayudar mucho).

Podríamos aplicar la técnica del ObjectMother que vimos en el post anterior, pero una limitación del ObjectMother es que su construcción de objetos es muy estática y nos obliga a añadir nuevas propiedades o métodos para obtener objetos en distintos estados, complicando el ObjectMother cada vez más:

public class ObjectMother
{
    public static readonly Task UnassignedTask ...
    public static readonly Task CompletedTask ...
    public static readonly Task TaskAssignedToAdminUser ...
    public static readonly Task TaskDueTomorrow ...
}

Poniendo los objetos como deben estar

Cuando necesitamos poder construir objetos en distintos estados y el patrón ObjectMother se nos queda corto, la mejor opción es recurrir a un Builder.

El Builder tiene una ventaja y es que no sólo nos permite encapsular la forma de construir el objeto, sino que también podemos ocultar la forma de ponerlo en un determinado estado.

Usando un Builder, el test anterior quedaría más o menos así:

[Test]
public void A_Completed_Task_Cannot_Be_Reassigned()
{
    Task task = Build.Task.Completed();

    Assert.Throws<InvalidOperationException>(() => task.Assign(ObjectMother.Marcos));
}

Normalmente suelo utilizar una clase que actúa como factoría estática para los builders de diferentes clases y un interface fluido en los propios builders con conversiones implícitas a los tipos construidos, con lo que el código de los tests queda bastante legible, como en el ejemplo anterior.

Lo bueno del Builder es que podemos crearlo incrementalmente, partiendo de un Builder muy simple que devuelve siempre el mismo objeto (al estilo del ObjectMother) e irlo complicando según nos van surgiendo nuevas necesidades en los tests.

Conclusiones

Tanto el ObjectMother como el Builder son patrones que nos permiten independizar los tests de cosas que no estamos testeando, haciéndolos menos frágiles y facilitando la refactorización del código.

El patrón Builder es más flexible que ObjectMother y todo lo que se puede conseguir con ObjectMother se puede hacer también con un Builder, pero requiere más código y es más complejo de implementar, por lo que hay casos en que no merece la pena.

Cuando la mayoría de los tests usen siempre un mismo objeto, sin importarle mucho su estado, ObjectMother es una buena salida. Si las necesidades varían mucho de un test a otro, usar un Builder es más productivo y da lugar a tests más legibles.

En realidad, el objetivo es siempre el mismo: tener tests sólidos, focalizados, que testeen lo que tienen que testear y que no se rompan cuando cambiamos comportamientos que no son los testeados por el test.