Test Dispatchers
Clase 8 de 16 • Curso de Android Testing
Resumen
La programación asíncrona en Android es fundamental para crear aplicaciones fluidas y responsivas. Sin embargo, testear componentes que utilizan corrutinas puede ser un desafío debido a su naturaleza no determinista. Dominar el arte de probar ViewModels que ejecutan corrutinas es esencial para garantizar la calidad y confiabilidad de nuestras aplicaciones.
¿Cómo testear ViewModels que utilizan corrutinas?
Cuando trabajamos con ViewModels en Android, frecuentemente implementamos operaciones asíncronas mediante corrutinas. Estas operaciones pueden incluir llamadas a APIs, acceso a bases de datos o cualquier tarea que no deba bloquear el hilo principal. Para testear estos componentes de manera efectiva, necesitamos controlar el comportamiento asíncrono de forma predecible.
El ViewModel que vamos a testear tiene las siguientes características:
- Depende de un repositorio de usuarios
- Recibe un ID de usuario a través de un SaveStateHandle
- Maneja estados de carga, éxito y error
- Utiliza corrutinas para cargar datos de perfil
Preparando el entorno de pruebas
Para comenzar, necesitamos crear un "fake" o doble de prueba para el repositorio de usuarios. Este enfoque nos permite simular diferentes escenarios sin depender de implementaciones reales:
class UserRepositoryFake : UserRepository {
// Datos de prueba
val user = User(
id = UUID.randomUUID().toString(),
username = "test_username"
)
val place = Place(
id = UUID.randomUUID().toString(),
name = "test_place",
coordinates = Coordinates(0.0, 0.0)
)
val profile = Profile(
user = user,
places = List(10) { place }
)
// Variables para controlar el comportamiento
var profileToReturn: Profile = profile
var errorToReturn: Exception? = null
override suspend fun getProfile(userId: String): Result<Profile> {
return if (errorToReturn != null) {
Result.failure(errorToReturn!!)
} else {
Result.success(profileToReturn)
}
}
override suspend fun getPlaces(): Result<List<Place>> {
return Result.success(places)
}
}
Este fake nos permite controlar exactamente qué datos o errores devolverá el repositorio durante nuestras pruebas.
Creando la clase de prueba para el ViewModel
Ahora podemos crear nuestra clase de prueba para el ProfileViewModel:
class ProfileViewModelTest {
private lateinit var viewModel: ProfileViewModel
private lateinit var repository: UserRepositoryFake
private val testDispatcher = StandardTestDispatcher()
@Before
fun setup() {
// Configuramos el dispatcher de prueba
Dispatchers.setMain(testDispatcher)
// Inicializamos el repositorio fake
repository = UserRepositoryFake()
// Creamos el ViewModel con el SaveStateHandle que simula la navegación
viewModel = ProfileViewModel(
repository = repository,
savedStateHandle = SavedStateHandle(
mapOf("userId" to repository.profileToReturn.user.id)
)
)
}
@After
fun tearDown() {
// Restauramos el dispatcher original
Dispatchers.resetMain()
}
}
Es importante destacar que estamos utilizando SavedStateHandle
para simular los parámetros de navegación, exactamente como lo haría el sistema real de navegación de Android.
El problema con los dispatchers en pruebas
Cuando intentamos ejecutar pruebas en ViewModels que utilizan corrutinas, nos encontramos con un problema: el viewModelScope
utiliza por defecto Dispatchers.Main
, que no está disponible en entornos de prueba unitaria.
Para solucionar esto, necesitamos reemplazar el dispatcher principal con uno específico para pruebas:
@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
// Resto del código...
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
Escribiendo casos de prueba efectivos
Ahora podemos escribir pruebas que validen el comportamiento de nuestro ViewModel:
@Test
fun `given valid userId when loadProfile then profile is loaded`() = runTest {
// Act
viewModel.loadProfile()
// Avanzamos hasta que no haya operaciones pendientes
testDispatcher.advanceUntilIdle()
// Assert
assertThat(viewModel.state.value.profile).isEqualTo(repository.profileToReturn)
assertThat(viewModel.state.value.isLoading).isFalse()
}
En esta prueba, verificamos que:
- El perfil se carga correctamente
- El estado de carga se actualiza a
false
cuando termina la operación
Controlando el flujo de tiempo en las pruebas
Existen dos enfoques principales para controlar el tiempo en pruebas de corrutinas:
- StandardTestDispatcher: Requiere control manual del tiempo mediante
advanceUntilIdle()
oadvanceTimeBy()
- UnconfinedTestDispatcher: Ejecuta las corrutinas inmediatamente sin necesidad de control manual
// Usando StandardTestDispatcher (control manual)
private val testDispatcher = StandardTestDispatcher()
@Test
fun testWithStandardDispatcher() = runTest {
viewModel.loadProfile()
testDispatcher.advanceUntilIdle() // Necesario para avanzar el tiempo
// Verificaciones...
}
// Usando UnconfinedTestDispatcher (ejecución inmediata)
private val testDispatcher = UnconfinedTestDispatcher()
@Test
fun testWithUnconfinedDispatcher() = runTest {
viewModel.loadProfile()
// No es necesario avanzar el tiempo manualmente
// Verificaciones...
}
Probando escenarios de error
Es igualmente importante probar cómo se comporta nuestro ViewModel cuando ocurren errores:
@Test
fun `given repository error when loadProfile then error is set`() = runTest {
// Arrange
repository.errorToReturn = Exception("Test exception")
// Act
viewModel.loadProfile()
testDispatcher.advanceUntilIdle()
// Assert
assertThat(viewModel.state.value.profile).isNull()
assertThat(viewModel.state.value.errorMessage).isEqualTo("Test exception")
assertThat(viewModel.state.value.isLoading).isFalse()
}
En esta prueba verificamos que:
- El perfil permanece nulo cuando ocurre un error
- El mensaje de error se establece correctamente
- El estado de carga se actualiza a
false
incluso cuando hay un error
¿Cómo mejorar la estructura de nuestras pruebas?
Repetir la configuración de dispatchers en cada clase de prueba puede volverse tedioso y propenso a errores. En la próxima clase, aprenderemos a crear una regla personalizada con TestWatcher
que automatice este proceso, haciendo nuestras pruebas más limpias y mantenibles.
Los test dispatchers son herramientas poderosas que nos permiten controlar con precisión el comportamiento asíncrono en nuestras pruebas. Dominar su uso es esencial para crear pruebas confiables y predecibles para componentes que utilizan corrutinas en Android.
¿Has tenido problemas al testear componentes asíncronos en tus aplicaciones? Comparte tus experiencias y dudas en los comentarios para seguir aprendiendo juntos.