Hey, I'm trying to unit test a callbackFlow and I ...
# coroutines
n
Hey, I'm trying to unit test a callbackFlow and I feel like I'm missing something. It's the kind of "snake biting its tail" problem. I found a working solution with
onStart
and
delay
but it's smelly. Source code:
Copy code
// I don't control the ConnectivityManager, it's from Android
class ConnectivityRepository(private val connectivityManager: ConnectivityManager) {

    fun isInternetAvailableFlow(): Flow<Boolean> = callbackFlow {
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                trySend(true)
            }

            override fun onLost(network: Network) {
                trySend(false)
            }
        }

        connectivityManager.registerDefaultNetworkCallback(networkCallback)

        awaitClose { connectivityManager.unregisterNetworkCallback(networkCallback) }
    }
}
Unit test:
Copy code
@Test
    fun `happy path`() = runTest {
        // Given
        val connectivityRepository = ConnectivityRepository(connectivityManager)

        val networkCallbackSlot = slot<ConnectivityManager.NetworkCallback>() // Mocking stuff : I can 'capture' something during the test execution with this
        val connectivityManager = mockk<ConnectivityManager>() // Mocking the ConnectivityManager
        justRun { connectivityManager.registerDefaultNetworkCallback(capture(networkCallbackSlot)) } // This function is mocked and "wired" with the slot
        justRun { connectivityManager.unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>()) }

        // When
        val result = connectivityRepositoryImpl.isInternetAvailableFlow().onStart { // Ugly solution but nothing else works...
            launch {
                delay(1)
                networkCallbackSlot.captured.onAvailable(mockk())
            }
        }.first()

        // Then
        assertThat(result).isTrue()
    }
I need to capture the anonymous class extending
ConnectivityManager.NetworkCallback
in order to call it with either
onAvaible
or
onLost
during my test, but at the same time, since this is a cold flow, it won't run until I collect it. But it won't emit something until I run
onAvaible
or
onLost
.
onStart
is too early, and
collect
is never called. I'd need a "afterStart" callback on my Flow or something like that ?
m
Turbine won't work here? Sth like
Copy code
connectivityRepositoryImpl.isInternetAvailableFlow().test {
   networkCallbackSlot.captured.onAvailable(mockk())
   awaitItem() shouldBe true
}
e
you can do the same without Turbine (although it's certainly easier with the library):
Copy code
produce(Dispatchers.UNCONFINED) {
    connectivityRepository.isInternetAvailableFlow().collect { send(it) }
}.consume {
    verify { connectivityManager.registerDefaultNetworkCallback(capture(networkCallbackSlot)) }
    networkCallbackSlot.captured.onAvailable(mockk())
    receive() shouldBeEqualTo true
}
👏 1
🙌 1
n
Kotlin testing 1.6 completely broke Turbine so I ditched it, guess I'll try produce / cosume (I didn't know this API yet, looks like a channel 😄 )
m
it didn't break Turbine, it works fine 🙂 and 0.8.0 has polished some rough edges around kotlin 1.6 testing afaik
n
Oh great, I should try it again. Last take on that topic was Jake Wharton wasn't happy about the new APIs concerning Turbine, so I left it until it stabilized a bit more.
@ephemient, care to explain why Unconfined dispatcher is explicitely needed ?
e
you could also stick a
runCurrent()
at the beginning of the
consume {}
block. it's to ensure that the
collect {}
in the
produce {}
block has run far enough that the collector has actually started before you try to make use of things that depend on it starting. if you're only doing tests on
receive()
then you don't need it since that'll suspend until there's a value.
compare to Turbine which ensures that the collector has started before running the
.test {}
block