https://kotlinlang.org logo
#coroutines
Title
# coroutines
n

Nino

06/21/2022, 12:02 PM
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

Michal Klimczak

06/21/2022, 1:28 PM
Turbine won't work here? Sth like
Copy code
connectivityRepositoryImpl.isInternetAvailableFlow().test {
   networkCallbackSlot.captured.onAvailable(mockk())
   awaitItem() shouldBe true
}
e

ephemient

06/21/2022, 2:01 PM
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

Nino

06/21/2022, 2:58 PM
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

Michal Klimczak

06/21/2022, 2:59 PM
it didn't break Turbine, it works fine 🙂 and 0.8.0 has polished some rough edges around kotlin 1.6 testing afaik
n

Nino

06/21/2022, 3:06 PM
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

ephemient

06/21/2022, 3:22 PM
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
5 Views