Test Doubles

Clase 5 de 16Curso de Android Testing

Resumen

La implementación de pruebas unitarias efectivas es fundamental para garantizar la calidad del software. Cuando buscamos aislar la lógica de nuestro código de las dependencias externas, los test doubles se convierten en herramientas indispensables. Estos nos permiten crear entornos de prueba controlados, predecibles y rápidos, evitando los problemas asociados con dependencias reales como bases de datos o servicios de red.

¿Qué son los test doubles y por qué son necesarios?

Los test doubles son objetos que reemplazan a las dependencias reales durante las pruebas unitarias. Su principal objetivo es permitirnos probar una unidad de lógica de forma aislada, sin que factores externos influyan en los resultados.

Imagina que tienes una clase llamada ShoppingCardCache que se conecta a una base de datos o API real. Si intentas probar un componente que depende de esta clase, te enfrentarás a varios problemas:

  • Las pruebas serán lentas debido a las conexiones externas
  • Los resultados pueden ser impredecibles
  • Pueden producirse fallos sin una razón clara (flaky tests)
  • Será difícil determinar si un fallo se debe a tu lógica o a la dependencia externa

La solución es reemplazar estas dependencias reales por versiones controladas que tú mismo defines, permitiendo pruebas rápidas, estables y verdaderamente unitarias.

¿Cuáles son los diferentes tipos de test doubles?

Existen varios tipos de test doubles, cada uno con propósitos específicos:

Dummy

Es el test double más simple. Se utiliza únicamente para cumplir con la firma de una interfaz, pero no tiene ninguna funcionalidad real.

public class ShoppingCartCacheDummy implements ShoppingCartCache {
    @Override
    public List<Product> loadCart() {
        return Collections.emptyList();
    }
    
    @Override
    public void saveCart(List<Product> products) {
        // No hace nada
    }
}

Los dummies son ideales cuando solo necesitas pasar un objeto como parámetro pero sabes que no se utilizará en la lógica que estás probando.

Stub

Un stub devuelve datos fijos y predefinidos. No implementa lógica real, simplemente responde con valores que tú has configurado previamente.

public class ShoppingCartCacheStub implements ShoppingCartCache {
    @Override
    public List<Product> loadCart() {
        // Siempre devuelve una lista con 2 productos predefinidos
        return Arrays.asList(
            new Product("Producto 1", 10.0),
            new Product("Producto 2", 20.0)
        );
    }
    
    @Override
    public void saveCart(List<Product> products) {
        // No hace nada
    }
}

Los stubs son perfectos cuando necesitas que tu código reciba una respuesta específica para probar cómo reacciona ante ella.

Fake

Un fake implementa una lógica funcional simplificada, generalmente manteniendo todo en memoria en lugar de usar recursos externos.

public class ShoppingCartCacheFake implements ShoppingCartCache {
    private List<Product> products = new ArrayList<>();
    
    @Override
    public List<Product> loadCart() {
        return new ArrayList<>(products);
    }
    
    @Override
    public void saveCart(List<Product> products) {
        this.products = new ArrayList<>(products);
    }
}

Los fakes son útiles cuando necesitas comportamientos realistas pero sin depender de bases de datos o servicios de red.

Spy

Un spy funciona como un fake pero además registra información sobre cómo se utilizó, permitiéndote verificar si los métodos fueron llamados correctamente.

public class ShoppingCartCacheSpy implements ShoppingCartCache {
    private List<Product> products = new ArrayList<>();
    private int loadCartCallCount = 0;
    
    @Override
    public List<Product> loadCart() {
        loadCartCallCount++;
        return new ArrayList<>(products);
    }
    
    @Override
    public void saveCart(List<Product> products) {
        this.products = new ArrayList<>(products);
    }
    
    public int getLoadCartCallCount() {
        return loadCartCallCount;
    }
}

Los spies son excelentes para verificar efectos secundarios o asegurarte de que ciertos métodos fueron invocados durante la ejecución.

Mock

Un mock es completamente controlado y te permite definir expectativas sobre cómo debe ser utilizado. Puedes especificar qué métodos deben llamarse, con qué parámetros y cuántas veces.

// Ejemplo usando un framework de mocking como Mockito
ShoppingCartCache mockCache = Mockito.mock(ShoppingCartCache.class);
Mockito.when(mockCache.loadCart()).thenReturn(Arrays.asList(new Product("Producto", 15.0)));

// Usar el mock...

// Verificar que loadCart fue llamado exactamente una vez
Mockito.verify(mockCache, Mockito.times(1)).loadCart();

Los mocks ofrecen el máximo control sobre el comportamiento de las dependencias, pero pueden hacer que las pruebas dependan demasiado de la implementación en lugar del comportamiento.

¿Cómo elegir el test double adecuado?

La elección del test double apropiado depende de lo que necesites probar:

  • Dummy: Cuando solo necesitas cumplir con una firma de método pero no se usará realmente.
  • Stub: Cuando necesitas que tu código reciba datos específicos para probar su comportamiento.
  • Fake: Cuando requieres una implementación funcional pero sin dependencias externas.
  • Spy: Cuando necesitas verificar que ciertos métodos fueron llamados correctamente.
  • Mock: Cuando necesitas control total sobre el comportamiento y las expectativas.

Es importante recordar que no todo debe ser un mock o un fake. Cada tipo tiene su propósito específico, y elegir el adecuado hará que tus pruebas sean más claras, mantenibles y efectivas.

Los test doubles son herramientas poderosas que nos permiten escribir pruebas unitarias de alta calidad, aislando perfectamente la lógica que queremos probar. Al dominar estos conceptos, podrás crear suites de pruebas rápidas, confiables y que realmente verifiquen el comportamiento de tu código sin interferencias externas. ¿Has utilizado alguno de estos test doubles en tus proyectos? Comparte tu experiencia en los comentarios.