Adios, Mocks

Tenía pensado contar algo sobre mis (escasos) avances con Javascript, pero una interesante discusión surgida en el blog de Jorge Serrano me ha hecho meterme en esto.

En respuesta a la encuesta de Jorge, yo decía:

La verdad es que, aunque he contestado RhinoMocks, cada vez uso menos mocks y trato de hacer otro tipo de tests.

A veces me acabo haciendo fakes a mano y me resulta más cómodo y más legible. Cuando usaba RhinoMocks acaba haciendo tests demasiado farragosos que no testeaban nada útil.

Como no le quiero robar su blog (que para eso es suyo) y en un comentario queda la cosa un poco triste, aprovecho para extenderme aquí.

Hace unos cuatro años, empecé a utilizar mocks y me parecieron algo fantástico. De repente podía testear fácilmente clases sin tener implementadas sus dependencias, sólo necesitaba definir el interface que usarían para comunicarse.

Esto me llevó a escribir un montón de tests que eran puramente de interacción:

  • Al invocar el método X del presenter
  • …se debe invocar el método Y del repository
  • …y se debe pasar el resultado a la vista
// Ejemplo de test *puro* de interacción
public void WhenThePresenterIsStarted_ThenTheCustomersAreLoadedIntoTheView
{
   var repository = MockRepository.GenerateStub<ICustomerRepository>();
   var view = MockRepository.CreateMock<ICustomersView>();
   var presenter = new CustomerListPresenter(view, repository);
  
   var customers = new List<Customer>();    

   repository.Stub(x => x.FindAll()).Return(customers);
   presenter.Start();
   view.AssertWasCalled(x => x.BindTo(customers));
}

En casos tan simples como el anterior solían funcionar bien pero, ¿aportaban algo? La verdad es que muy poco. Siendo realistas, en un método así es casi igual de fácil equivocarse al escribir el test (se me ha olvidado poner el repository.Stub(), he pasado mal el parámetro en view.AssertWasCalled()) que equivocarse al implementar la clase. Y este es un caso fácil. Cuando lo que quieres testear es algo sólo un poco más complicado:

  • Cuando se invoca el método descontinuar producto del presenter
  • Se busca el producto por id
  • Se llama a descontinuar
  • Se notifica al almacén que el producto ha sido descontinuado

El test resultante es éste:

public void WhenTheUserDiscontinuesAProduct_ThenTheProductIsDiscountinuedAndWarehouseIsNotified()
{
    var warehouse = MockRepository.CreateMock<IWarehouse>();
    var repository = MockRepository.CreateMock<IProductRepository>();
    var presenter = new ProductPresenter(repository, warehouse);

    var productId = 19;
    var product = new Product(productId, "Sample Product");
    
    var product = repository.Stub(x => x.FindById(productId))
                    .Returns(product);
    
    presenter.DiscontinueProduct(productId);
    
    // Aquí tengo una dependencia temporal. No me sirve escribir 
    //     Assert.IsTrue(product.IsDiscontinued);
    //     warehouse.AssertWasCalled(
    //         x => x.ProductHasBeenDiscontinued(product);
    // porque necesito asegurarme de que el product es descontinuado
    // antes de notificárselo a Warehouse
    // La única forma de testearlo correctamente sin cambiar
    // el diseño es ésta: 
    
    warehouse.AssertWasCalled(
        x => x.ProductHasBeenDiscontinued(
            Arg.Matches(p => p == product && p.IsDiscontinued))
}

A mi, sinceramente, el AssertWasCalled del final me parece un poco críptico.

Un leve incremento en la complejidad del método a testear se traduce directamente en mayor complejidad en los tests: la parte arrange del Arrange/Act/Assert se puede complicar rápidamente, y la parte assert también, como vemos en el ejemplo anterior.

Esto es algo que con los tests de estado no suele pasar, un test de estado al final se suele limitar a una tabla de verdad: si las entradas son estas, la salida (o el estado observable) debe ser esto.

Cómo se realizan esos cambios de estado es algo que queda interno al objeto. A mi me da lo mismo. Él se lo guisa y él se lo come. Es verdad que a veces conseguir poner el objeto que estamos testeando en el estado inicial no es fácil, pero en general suele ser más sencillo (y hay más alternativas para hacerlo cómodamente) que en los tests de interacción.

En un test con Mocks necesitariamente estamos conociendo algo de la estructura interna de la clase (las llamadas que hacemos a los mocks) y si queremos cambiar eso se nos va a romper todo, aunque semánticamente no haya cambiado nada y el código siga funcionando.

Veamos un ejemplo de lo anterior. Imaginemos una método que se encarga de bloquear un conjunto de usuarios:

public void BlockUsers(int[] userIds)
{
   for (var id in userIds)
   {
       var user = repository.FindById(id);
       user.Block();
   }
}

Para testear esto con mocks, se pondrían una serie de Stubs sobre los ids y luego se verificaría que los usuarios cargados con esos ids han sido bloqueados.

Ahora bien, supongamos por algún motivo decidimos cambiar la forma de cargar los usuarios. Por ejemplo para mejorar la eficiencia decidimos usar un método FindByIds(int[] userIds), y cambiamos el método original por lo siguiente:

public void BlockUsers(int[] userIds)
{
    var users = repository.FindByIds(userIds);
    foreach (var user in users)
        user.Block();
}

¿Qué ha pasado? Con esto nos acabamos de cargar los tests, y además no nos enteraremos hasta que los ejecutemos. ¿Es razonable que se rompan? Pues hombre, por una parte, hemos cambiado la clase que estamos testeando, así que tener que hacer un ajuste en los tests parece normal.

Pero por otro lado, la lógica que estamos aplicando, lo que realmente hace el método, no ha cambiado nada. Sigue recibiendo una lista de ids y bloqueando los usuarios con esos ids. En otras palabras, lo que hemos hecho ha sido una refactorización “de libro”. Hemos cambiado la forma sin cambiar el comportamiento. Sin embargo, mis tests, aquellos que tengo escritos entre otras cosas como red de seguridad al refactorizar no sólo no me han servido sino que se me han roto.

Los tests de interacción no cumplen uno de sus objetivos fundamentales: permitir la refactorización segura.

¿Cómo podríamos haber evitado esto?

Una opción muy sencilla es no testear ese método. ¡Herejía! ¡Sacrilegio! Pues no. Debo confesar que hay cosas que no testeo, porque la rentabilidad que obtengo de los tests no es suficiente. Tal vez sean partes muy complicadas de testear, o partes muy simples y poco sensibles a errores, o partes que raramente se modifican.

Una opción que suelo emplear últimamente es hacerme fakes a mano. Un fake es una implementación simulada de la dependencia. Para que funcione, un fake tiene que ser:

  • Fácil de implementar y mantener. No tendría sentido que al final acabásemos con bugs en los fakes.
  • Fácil de usar. Si utilizar el fake es complicado, no tiene sentido. Para eso es mejor emplear una librería de Mocks directamente.
  • Rápido al ejecuarse. Si tenemos un fake de una base de datos, parte su utilidad es evitar la lentitud de preparar la base de datos y ejecutar el test contra ella.

La ventaja es que el fake es que puede ser una implementación “completa” del interface que estamos simulando, por lo que si el cliente del fake, es decir, si la clase que estamos testeando, decide cambiar la forma de usarlo, seguirá funcionando todo.

En el caso anterior, podríamos tener un fake del IUserRepository con una lista de usuarios en memoria:

public class UserRepositoryFake : IUserRepository
{
    // Es un fake. Las reglas de encapsulado se relajan... 
    // Dejo que los usuarios del fake (los tests) puedan
    // toquetear todo lo que quieran los datos de mentira
    // accediendo al atributo users.
    public readonly List users = new List();
   
    public User FindById(int id)
    {
        return users.First(x => x.Id == id);
    }  

    public IEnumerable FindByIds(int[] ids)
    {
        return users.Where(x => ids.Any(y => y == x));
    }
}

Sé que ha simple vista parece más trabajo, y realmente puede ser más código que preparar los stubs con los mocks, pero a mi al final me parece que el código queda más claro, más legible y más resistente a los cambios. Además, hoy en día con herramientas como Resharper crear una implementación de un interface son 4 pulsaciones de teclas.

Yo suelo usar fakes propios para cada “paquete de tests”, entiendo como paquete de tests uno o más TestFixtures (en terminología NUnit) que tienen cierta coherencia entre sí.

De todas formas, cada vez intento testear menos clases de este estilo (presenters, controllers y similares) porque el rendimiento que obtengo es muy bajo. Intento pasar toda la lógica de la aplicación a clases que sean fáciles de testear y cuyos tests que me garanticen algo más que el flujo de llamadas entre un par de componentes.

Todo esto no quiere decir que usar mocks sea malo per se, al contrario. Para llegar a esta conclusión, yo he tenido que usarlos, y mucho. Es difícil encontrar el punto de equilibrio para cada uno, y seguramente dependa de tantos factores que no hay una verdad absoluta, pero lo que sí tengo claro, es que si he conseguido aplicar TDD, y disfrutar de los beneficios que conlleva, ha sido en gran parte gracias a haber conocido en su momento los mocks y haber empezado a usarlos.