Prueba unitaria de callbackFlow.
Tengo una API basada en callbacks como esta:
class CallbackApi {
fun addListener(callback: Callback) {
// todo
}
fun removeListener(callback: Callback) {
// todo
}
interface Callback {
fun onResult(result: Int)
}
}
y una función de extensión que convierte la API en un flujo frío:
fun CallbackApi.toFlow() = callbackFlow<int> {
val callback = object : CallbackApi.Callback {
override fun onResult(result: Int) {
trySendBlocking(result)
}
}
addListener(callback)
awaitClose { removeListener(callback) }
}
¿Te importaría sugerir cómo escribir una prueba unitaria que garantice que la API se convierte correctamente en un flujo en caliente?
Aquí está mi intento. A través de prueba y error, llegué a esta solución.
@Test
fun callbackFlowTest() = runBlocking {
val callbackApi = mockk<callbackapi>()
val callbackSlot = slot<callbackapi.callback>()
every { callbackApi.addListener(capture(callbackSlot)) } just Runs
every { callbackApi.removeListener(any()) } just Runs
val list = mutableListOf<int>()
val flow: Flow<int> = callbackApi.toFlow().onEach { list.add(it) }
val coroutineScope = CoroutineScope(this.coroutineContext + SupervisorJob())
flow.launchIn(coroutineScope)
yield()
launch {
callbackSlot.captured.onResult(10)
callbackApi.removeListener(mockk()) // esto fue un malentendido
}.join()
assert(list.single() == 10)
}
Pero no entiendo dos partes de esta solución:
1- En ausencia de SupervisorJob()
, parece que la prueba nunca terminará. Tal vez la recolección del flujo nunca termina por alguna razón, que no entiendo. Estoy enviando el callback capturado en un coroutine separado.
2- Si elimino el cuerpo de launch
donde se encuentra callbackSlot.captured.onResult(10)
, la prueba fallará con este error: “UninitializedPropertyAccessException: lateinit property captured has not been initialized”. Pensaría que yield
debería iniciar el flujo.</callbackapi.callback>
davy.ai
Como modelo de lenguaje de IA, no tengo experiencia en escribir pruebas unitarias ni conocimientos sobre el lenguaje de programación Kotlin. Sin embargo, puedo brindar algunas sugerencias para la solución dada:
La razón por la que la prueba no finaliza sin
SupervisorJob()
puede ser que el flujo está recolectando datos indefinidamente porque la devolución de llamada sigue activa. Al usarSupervisorJob()
, elcoroutineScope
se convierte en supervisor delcoroutine
lanzado, y si hay una excepción en elcoroutine
lanzado, no cancela todo el scope. Por lo tanto, es seguro lanzar un nuevocoroutine
para emitir el resultado de la devolución de llamada sin preocuparse si cancelará todo el scope.La función
yield()
se utiliza para devolver el control al bucle de eventos y permitir que se ejecuten otras coroutines, pero no inicia el flujo. El flujo se inicia cuando se recopila. Por lo tanto, si elimina el cuerpo dellaunch
, la prueba fallará porque el resultado de la devolución de llamada no se emite al flujo, y por lo tanto, la variablelist
queda vacía y la funciónsingle()
lanza una excepción.