Testing de Repository

Clase 6 de 16Curso de Android Testing

Resumen

La implementación de pruebas unitarias en clases con dependencias externas es un desafío común en el desarrollo de software. Cuando trabajamos con componentes que dependen de servicios externos como APIs, necesitamos estrategias que nos permitan probar nuestro código de manera aislada, rápida y predecible. Los test dobles, particularmente los "fakes", son una solución elegante a este problema, permitiéndonos simular el comportamiento de dependencias externas sin realizar llamadas reales a servicios externos.

¿Qué son los test dobles y por qué son importantes?

Los test dobles son objetos que reemplazan a componentes reales en nuestras pruebas. Cuando una clase depende de otra, como un repositorio de usuarios que depende de una API, no siempre queremos usar la implementación real durante las pruebas, especialmente si implica llamadas de red. Estas son algunas razones por las que los test dobles son cruciales:

  • Control total: Tú decides qué datos devuelve la dependencia y qué errores lanza.
  • Velocidad: Las pruebas se ejecutan más rápido al evitar operaciones lentas como llamadas de red.
  • Predictibilidad: El comportamiento es consistente y no depende de factores externos.
  • Cobertura: Puedes probar todos los caminos posibles del código, incluyendo manejo de errores.

Tipos de test dobles

Aunque en este artículo nos enfocamos en los "fakes", existen varios tipos de test dobles:

  • Fakes: Implementaciones simplificadas pero funcionales que reemplazan componentes reales.
  • Mocks: Objetos preprogramados con expectativas sobre cómo serán llamados.
  • Stubs: Proporcionan respuestas predefinidas a llamadas específicas.
  • Dummies: Objetos que se pasan pero nunca se utilizan realmente.
  • Spies: Registran las llamadas que reciben para verificarlas posteriormente.

¿Cómo implementar un fake para una API en Kotlin?

Vamos a ver cómo crear un fake para una API de usuarios. El objetivo es reemplazar las llamadas de red reales con datos predefinidos que podamos controlar en nuestras pruebas.

Creando la clase UserFakeAPI

Lo primero es crear una clase que implemente la misma interfaz que nuestra API real:

class UserFakeAPI : UserAPI {
    // Creamos una lista de usuarios de prueba
    private val users = (1..10).map { 
        User(
            id = it.toString(),
            userName = "user$it"
        )
    }
    
    // Creamos una lista de lugares de prueba
    private val places = (1..10).map { 
        Place(
            id = UUID.randomUUID().toString(),
            name = "place$it",
            coordinates = Coordinates(0.0, 0.0)
        )
    }
    
    // Implementamos el método para obtener un usuario
    override fun getUser(userId: String): User {
        return users.find { it.id == userId } 
            ?: throw Exception("User not found")
    }
    
    // Implementamos el método para obtener lugares
    override fun getPlaces(placeId: String): List<Place> {
        return places.filter { it.id == placeId }
    }
    
    // Implementamos el método para obtener el perfil completo
    override fun getProfile(userId: String): Profile {
        val user = getUser(userId)
        return Profile(
            user = user,
            places = getPlaces(userId)
        )
    }
}

Aspectos clave de esta implementación:

  1. Creamos datos de prueba predefinidos (usuarios y lugares).
  2. Implementamos los mismos métodos que la API real.
  3. Usamos lógica simple para simular el comportamiento de la API.
  4. Manejamos casos de error (como usuario no encontrado).

Ventajas de usar Kotlin plano

Es importante destacar que estamos usando Kotlin plano sin dependencias de Android. Esto tiene varias ventajas:

  • Velocidad: Las pruebas se ejecutan mucho más rápido.
  • Independencia: No dependemos del framework de Android.
  • Simplicidad: El código es más sencillo y fácil de entender.

¿Cómo escribir pruebas utilizando nuestro fake?

Una vez que tenemos nuestro fake, podemos usarlo para probar nuestro repositorio de usuarios:

class UserRepositoryTest {
    // Sujeto bajo prueba
    private lateinit var userRepository: UserRepositoryImplementation
    
    // Dependencia simulada
    private lateinit var userFakeAPI: UserFakeAPI
    
    @Before
    fun setup() {
        // Inicializamos nuestro fake
        userFakeAPI = UserFakeAPI()
        
        // Inicializamos el repositorio con el fake
        userRepository = UserRepositoryImplementation(userFakeAPI)
    }
    
    @Test
    fun `given valid user ID when get profile with fake API then returns profile`() {
        // Given
        val userId = "1"
        
        // When
        val profileResult = userRepository.getProfile(userId)
        
        // Then
        assertTrue(profileResult.isSuccess)
        
        // Verificamos que el ID del usuario sea correcto
        assertEquals(userId, profileResult.getOrThrow().user.id)
        
        // Verificamos que los lugares sean los esperados
        val expectedPlaces = userFakeAPI.places.filter { it.id == userId }
        assertEquals(expectedPlaces, profileResult.getOrThrow().places)
    }
}

Estructura de la prueba

Nuestra prueba sigue el patrón Given-When-Then (o Arrange-Act-Assert):

  1. Given (Dado): Establecemos las precondiciones, como el ID de usuario.
  2. When (Cuando): Ejecutamos la acción que queremos probar.
  3. Then (Entonces): Verificamos que el resultado sea el esperado.

Verificaciones importantes

En nuestra prueba verificamos:

  • Que el resultado sea exitoso (isSuccess).
  • Que el ID del usuario en el resultado coincida con el solicitado.
  • Que los lugares en el resultado sean los esperados.

Probando escenarios de error

Una de las grandes ventajas de los fakes es que podemos probar fácilmente escenarios de error. Por ejemplo, si intentamos obtener un usuario que no existe:

@Test
fun `given invalid user ID when get profile then returns failure`() {
    // Given
    val userId = "25" // ID fuera del rango definido en nuestro fake
    
    // When
    val profileResult = userRepository.getProfile(userId)
    
    // Then
    assertTrue(profileResult.isFailure)
    // Podríamos verificar también el tipo de excepción si fuera necesario
}

En este caso, nuestro fake lanzará una excepción porque el usuario no existe, y podemos verificar que el repositorio maneje correctamente este error.

Consideraciones para funciones suspendidas

Es importante mencionar que muchas de estas dependencias y funciones en aplicaciones reales son suspend functions, lo que significa que corren de forma asíncrona. Para probar código asíncrono, necesitamos herramientas adicionales como runBlockingTest o runTest de las bibliotecas de corrutinas de Kotlin.

Los test dobles, y en particular los fakes, son herramientas poderosas para probar clases con dependencias externas. Nos permiten controlar completamente el comportamiento de componentes externos y validar que nuestra lógica reacciona correctamente ante diferentes escenarios. Al implementar estas técnicas, podemos crear pruebas más rápidas, confiables y completas.

¿Has implementado test dobles en tus proyectos? ¿Qué estrategias utilizas para probar código con dependencias externas? Comparte tu experiencia en los comentarios.