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.