A question about using com.apollographql.apollo3.m...
# apollo-kotlin
s
A question about using com.apollographql.apollo3.mockserver.MockServer inside a test rule while having to call their suspending functions.
I’ve found that I’m repeating this exact snippet
Copy code
private lateinit var mockServer: MockServer
private lateinit var apolloClient: ApolloClient

private suspend fun before() {
    mockServer = MockServer()
    apolloClient = ApolloClient.Builder().customScalarAdapters(CUSTOM_SCALAR_ADAPTERS).serverUrl(mockServer.url()).build()
}

private suspend fun after() {
    apolloClient.close()
    mockServer.stop()
}
in every test that I want to use com.apollographql.apollo3.mockserver.MockServer to run some tests and I thought maybe I could just make a rule out of this like
Copy code
class ApolloMockServerRule() : ExternalResource() {
    lateinit var mockServer: MockServer
    lateinit var apolloClient: ApolloClient

    override fun before() {
        mockServer = MockServer()
        apolloClient =
            ApolloClient.Builder().customScalarAdapters(CUSTOM_SCALAR_ADAPTERS).serverUrl(mockServer.url()).build()
    }

    override fun after() {
        apolloClient.close()
        mockServer.stop()
    }
}
But since
url()
and
stop()
are both suspending functions I can’t quite do it in a rule unless I just start both before() and after() inside
runBlocking
. Is there any alternative I should be doing here? Or should I just copy paste that snippet around? I guess it’s not that much, but it’d just be convenient to avoid having it in this growing number of tests that use it.
m
You could always define your own
myTestFunction(block: suspend (MockServer, ApolloClient) -> Unit))
?
Or else maybe KoTest or something else supports suspend in rules but I'm not overly familiar with those
s
Yeah that’s right, doing this approach actually does work:
Copy code
class ApolloMockServerRule : ExternalResource() {
    fun runTest(block: suspend TestScope.(MockServer, ApolloClient) -> Unit) {
        kotlinx.coroutines.test.runTest {
            val mockServer = MockServer()
            val apolloClient = ApolloClient.Builder()
                .customScalarAdapters(CUSTOM_SCALAR_ADAPTERS)
                .serverUrl(mockServer.url())
                .build()
            block(mockServer, apolloClient)
            apolloClient.close()
            mockServer.stop()
        }
    }
}
And on the call site:
Copy code
@get:Rule
val apolloMockServerRule: ApolloMockServerRule = ApolloMockServerRule()

fun `test`() = apolloMockServerRule.runTest { mockServer, apolloClient ->
}
But now I realize this doesn’t have to be a rule at all 😅 Could just be a top level function somewhere in my test source set 😄
😄 1
Okay just for discoverability and easily differentiating it from kotlinx runTest I’ll just do
Copy code
object ApolloMockServer {
    fun runTest(block: suspend TestScope.(MockServer, ApolloClient) -> Unit) {
        return kotlinx.coroutines.test.runTest {
            val mockServer = MockServer()
            val apolloClient = ApolloClient.Builder()
                .customScalarAdapters(CUSTOM_SCALAR_ADAPTERS)
                .serverUrl(mockServer.url())
                .build()
            block(mockServer, apolloClient)
            apolloClient.close()
            mockServer.stop()
        }
    }
}
and call it by
Copy code
@Test
fun `test`() = ApolloMockServer.runTest { mockServer, apolloClient ->
}
Or maybe I can think of a better name than ApolloMockServer, but that’s easy to change anyway. Thanks for making me think a bit smarter 😅
👍 1
A super important detail that I figured out only today about using Apollo and the new
runTest
from kotlinx.coroutines. I am posting it here in hopes that if anyone sees this thread they don’t face the same problem. Also for me to get back to whenever I see all this and wonder what I was doing 😄 The builder chain needs to be updated to this (the requestedDispatcher part, the
extraApolloClientConfiguration
is just something I did for my need of adding cache for example in some test):
Copy code
fun runApolloTest(
    extraApolloClientConfiguration: ApolloClient.Builder.() -> ApolloClient.Builder = { this },
    block: suspend TestScope.(MockServer, ApolloClient) -> Unit,
) {
    return kotlinx.coroutines.test.runTest {
        val mockServer = MockServer()
        val apolloClient = ApolloClient.Builder()
            .requestedDispatcher(StandardTestDispatcher(testScheduler)) // <-- HERE
            .customScalarAdapters(CUSTOM_SCALAR_ADAPTERS)
            .serverUrl(mockServer.url())
            .extraApolloClientConfiguration()
            .build()
        block(mockServer, apolloClient)
        apolloClient.close()
        mockServer.stop()
    }
}
So why
StandardTestDispatcher(testScheduler)
? (I guess UnconfinedTestDispatcher would also work but didn’t test that, I needed the Standard one for my case) Initially I had a problem of tests hanging when running tests with the ViewModel, when trying to execute an Apollo query inside the launch of viewModelScope. Since that one uses Dispatcher.Main.immediate as a dispatcher. To do that I added this test rule as suggested in the Android docs and set
StandardTestDispatcher()
as the requestedDispatcher in the ApolloClient. This makes the tests that have the MainCoroutineRule pass. But it made the tests that do not use the MainCoroutineRule hang. That is cause if you set the main dispatcher the
StandardTestDispatcher
function gets the scheduler from there, however if not it makes it’s own scheduler (same line) so it’s not the same as the one provided in the
kotlinx.coroutines.test.runTest
so the coroutines never run again 🥴 Then changing it to
.requestedDispatcher(StandardTestDispatcher(testScheduler))
seems to be the correct solution that solves all of these at the same time. Damn these testing APIs are quite tricky huh 😅
m
Not 100% sure about the details there but is there something we can do in the lib to help with that?
We should certainly rename
requestedDispatcher
->
dispatcher
because the "requested" is from native where the choice is not always honoured but it shouldn't be an issue with the new memory model
But besides that, "feels" like
execute()
should dispatch in
<http://Dispatchers.IO|Dispatchers.IO>
by default
s
requestedDispatcher -> dispatcher is a good idea, yes. About doing anything on the library level, no I can’t think of anything really myself. For the tests you need to use the “correct” dispatcher in order to get the auto skip on delays and being able to run
runCurrent
and such. And since Apollo automatically switches the dispatcher it in order to be safe to call
execute
from the Main dispatcher I guess we simply have to pass the correct dispatcher to
requestedDispatcher/dispatcher
so that in tests that one is used instead.
execute
dispatching on
IO
by default is definitely a good thing that shouldn’t change as I see it. I think other libraries do this automatic changing of dispatchers as well right, like Room or Retrofit. I wonder if they also have the same problem in tests.
👍 1