Cómo NO escribir tests unitarios: Dependiendo de otros constructores

Siguiendo con mis posts sobre cosas que he hecho mal y me han fastidiado después al escribir tests, después de explicar los problemas que se producen cuando replicas la lógica testeada en el propio test, hoy toca hablar de otro problemilla: el (ab)uso de constructores.

Para poder escribir cualquier tipo de test necesitamos preparar un escenario y para eso necesitamos crear objetos. Como mínimo, hará falta crear el objeto que vamos a testear, pero probablemente también sea necesario crear sus dependencias, los parámetros de los métodos que vamos a invocar, etc.

Llegado el momento de crear un objeto lo más frecuente es emplear un constructor, y si hacemos las cosas de forma ortodoxa, el constructor debería recibir los parámetros mínimos que permitan a la clase mantener su invariante.

Es decir, si no pueden existir usuarios sin nombre, en un mundo ideal el constructor de la clase usuario debería recibir, al menos, un parámetro nombre (ya lo sé, luego llegan los ORMs con sus imposiciones y acabas con un constructor sin parámetros, pero esa es otra historia…).

Si estás aplicando DDD, el uso de constructores es todavía más importante y frecuente dentro de los objetos de dominio, tanto entidades, como especialmente value objects, ya que estos son inmutables y, por tanto, deben quedar plenamente inicializados en el constructor.

Demasiado constructor

En el código de la aplicación puede haber bastantes puntos en los que se creen objetos, pero créeme, en los tests va a haber muchísimos más, sobre todo para ciertos tipos de objetos que se necesitan en muchos tests.

Por ejemplo, en cualquier aplicación que gestione usuarios, el constructor de la clase User seguramente sólo se invoque en un par de sitios: el registro de un nuevo usuario y la carga de un usuario desde la base de datos, y probablemente este segundo punto ni siquiera esté en nuestro código porque haya un ORM que se esté encargando de ello.

Sin embargo, habrá un montón de clases o métodos que utilicen objetos usuario de una forma u otra, y cuando escribamos los tests para ellos necesitaremos construir objetos usuarios de prueba. En esos tests sólo nos interesa tener un objeto creado, nos da igual como se construya porque no forma parte de lo queremos testear, y muchas veces ni siquiera nos importa el estado en que se encuentre.

Un ejemplo sería algo parecido a esto:

[TestFixture]
public void When_a_task_is_assigned_its_state_changes_to_assigned()
{
    var task = new Task("Do something weird");
    var user = new User("lucas", "lucas@gmail.com");

    task.AssignTo(user);

    Assert.That(task.Status, Is.EqualTo(TaskStatus.Assigned));
}

En este caso el valor de user no nos importa, sólo lo necesitamos para poder invocar el método AssignTo, que es lo que realmente estamos testeando.

El problema de crear el objeto user a mano es cuando unas semanas después tenemos 149 usos del constructor en los tests y necesitamos cambiarlo para añadir un nuevo parámetro o cambiar alguno de los existentes.

Llegado ese momento, la única salida es cruzar los dedos y ver hasta dónde te pueden echar una mano herramientas como ReSharper, y si estás trabajando con un lenguaje estático, dar las gracias por tener un compilador que va a capturar los errores que introduzcas.

También puedes crear un nuevo constructor y empezar a enlazar constructores unos con otros, pero ¿realmente quieres ir dejando un rastro de cadáveres con constructores que sólo se utilizan en los tests? No parece una buena idea.

Que los construya su madre

Para evitar esto podemos emplear el patrón ObjectMother, que consiste en tener una clase encargada de proporcionarnos instancias de distintos objetos que usamos frecuentemente en los tests, es decir, una factoría de objetos pensados concretamente para nuestros tests.

Para el caso anterior, podríamos tener algo así:

public class ObjectMother
{
    public static readonly User Lucas
    {
        get { return new User("Lucas", "lucas@gmail.com", Role.User); }
    }

    public static readonly User Admin
    {
        get { return new User("Lucas", "lucas@gmail.com", Role.Admin); }
    }
}

La ventaja es que ahora siempre que necesitemos un objeto User podemos usar el ObjectMother y obtener una instancia lista para trabajar con ella, y si en algún momento cambia la forma de construirlo, sólo hay que tocar un punto.

Si necesitamos crear objetos inicializados de distinta forma, podemos añadir más propiedades al ObjectMother como en el ejemplo anterior, que nos permite crear usuarios normales o administradores.

Una limitación a tener en cuenta con esta técnica es que introduce una dependencia oculta entre todos los tests que usan el ObjectMother, ya que dependen de construir el objeto en un estado determinado.

Cuando hay que usar el constructor

Hay una situación en la que sí es importante usar el constructor del objeto: cuando lo que estamos testeando es el propio constructor. En ese caso nos interesa escribir el test de la forma más explícita posible y ver todos los parámetros que recibe el constructor.

Además, si cuando testeamos una clase empleamos sus constructores nos ayudará a identificar mejor problemas con constructores que reciben excesivos parámetros o cuyos parámetros son poco claros.

Conclusiones

Para diseñar un buen modelo de objetos es fundamental hacer un uso correcto de constructores que nos permitan obtener objetos válidos listos para ser usados.

Conforme avanzamos en la implementación del modelo, surgirán nuevos conceptos que se materializarán en nuevas clases con sus propios constructores, dando lugar a cambios en los constructores de los objetos que ya tenemos.

Esta refactorización no debe suponer un gran impacto en nuestros tests, para que podamos hacerla cómodamente y no nos frene la pereza de tener que actualizar decenas (o cientos) de tests. Por ello, lo mejor es evitar la dependencia directa de los tests sobre los constructores de los objetos que utilizan.

Hay veces que sólo el constructor no nos sirve para obtener un objeto en el estado que necesitamos para un test y necesitamos invocar métodos sobre el objeto antes de poder usarlo como parámetro en un test. En esos casos necesitamos algo más que un ObjectMother como el que hemos visto en este post, pero eso lo dejo para el próximo día.

2 comentarios en “Cómo NO escribir tests unitarios: Dependiendo de otros constructores

  1. Una duda, ¿para el test que se propone aquí en el cual se testea Task, no es mejor crear un mock User con los getName y getEmail mockeados? Así nos olvidaríamos de constructores.

  2. Hola Xavi,

    Es una opción. Dependiendo del lenguaje, puede ser más o menos complicado (por ejemplo en C# te obligaría a hacer que getName y getEmail fuesen virtuales), pero desde luego es una opción viable.

    Aun así, a mi personalmente me gusta menos porque requiere más código que mete ruido en el test (crear el mock y preparar el valor de retorno).

    Un saludo,

    Juanma

Comentarios cerrados.