Stylianos Gakis
06/21/2022, 12:05 PMprivate 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
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.mbonnin
06/21/2022, 12:14 PMmyTestFunction(block: suspend (MockServer, ApolloClient) -> Unit))
?Stylianos Gakis
06/21/2022, 12:44 PMclass 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:
@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 😄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
@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 😅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):
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 😅mbonnin
06/22/2022, 3:47 PMrequestedDispatcher
-> 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 modelexecute()
should dispatch in <http://Dispatchers.IO|Dispatchers.IO>
by defaultStylianos Gakis
06/23/2022, 7:14 AMrunCurrent
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.