Using the test artifact with `registerTestResponse...
# apollo-kotlin
s
Using the test artifact with
registerTestResponse
etc. I have a test where I am testing a ViewModel, some action happens, some state is set to being in a "loading" state, so I want to be able to assert that, and then I want to be able to add the response to the ApolloClient, so that it will return that value back to the suspend function which is waiting for it, so that I can also assert what happens after the network has responded. What happens now instead is that I call the action on my ViweModel, the state is set to be in this pending state, then the suspend function tries to hit the ApolloClient, it does not find the response registered in the test
TestNetworkTransport
so it throws with
No response registered for operation ...
. Outside of Apollo, I typically make this work using Turbines, where if someone does an
awaitItem()
on it, it will not immediately throw, but it will wait a bit to see if someone will add a response to that turbine, and when it's there then it gives it to the caller who was waiting for it in a suspending manner. Does what I say here make sense? I can try to explain it a bit better if not ๐Ÿ˜Š
Maybe what I want is to take this https://github.com/apollographql/apollo-kotlin/blob/5e780aa667dbcc9127e983f421ce4a[โ€ฆ]kotlin/com/apollographql/apollo/testing/TestNetworkTransport.kt and replace the
private val operationsToResponses = mutableMapOf<Operation<out Operation.Data>, TestResponse>()
with a turbine myself, which will wait if the item is not there yet, instead of throwing. Will try that
Copy code
private sealed interface TestResponse {
  object NetworkError : TestResponse

  class Response(val response: ApolloResponse<out Operation.Data>) : TestResponse
}

@ApolloExperimental
private class TurbineMapTestNetworkTransport : NetworkTransport {
  private val lock = reentrantLock()
  private val operationsToTurbineResponses: MutableMap<Operation<out Data>, Turbine<TestResponse>> =
    mutableMapOf<Operation<out Operation.Data>, Turbine<TestResponse>>()

  override fun <D : Operation.Data> execute(request: ApolloRequest<D>): Flow<ApolloResponse<D>> {
    return flow {
      // "Emulate" a network call
      yield()

      val response = lock.withLock {
        operationsToTurbineResponses.getOrPut(request.operation) {
          Turbine<TestResponse>(name = "Turbine for operation ${request.operation.name()}")
        }
      }.let {
        it.awaitItem()
      }

      val apolloResponse = when (response) {
        is TestResponse.NetworkError -> {
          ApolloResponse.Builder(operation = request.operation, requestUuid = request.requestUuid)
            .exception(exception = ApolloNetworkException("Network error registered in MapTestNetworkTransport"))
            .build()
        }

        is TestResponse.Response -> {
          @Suppress("UNCHECKED_CAST")
          response.response as ApolloResponse<D>
        }
      }

      emit(apolloResponse.newBuilder().isLast(true).build())
    }
  }

  fun <D : Operation.Data> register(operation: Operation<D>, response: ApolloResponse<D>) {
    lock.withLock {
      operationsToTurbineResponses.getOrPut(operation) {
        Turbine<TestResponse>(name = "Turbine for operation ${operation.name()}")
      }
    }.also {
      it.add(TestResponse.Response(response))
    }
  }

  fun <D : Operation.Data> registerNetworkError(operation: Operation<D>) {
    lock.withLock {
      operationsToTurbineResponses.getOrPut(operation) {
        Turbine<TestResponse>(name = "Turbine for operation ${operation.name()}")
      }
    }.also {
      it.add(TestResponse.NetworkError)
    }
  }

  override fun dispose() {}
}

@ApolloExperimental
fun <D : Operation.Data> ApolloClient.registerSuspendingTestResponse(
  operation: Operation<D>,
  response: ApolloResponse<D>,
): Unit = (networkTransport as? TurbineMapTestNetworkTransport)?.register(operation, response)
  ?: error("Apollo: ApolloClient.registerSuspendingTestResponse() can be used only with TurbineMapTestNetworkTransport")

@ApolloExperimental
fun <D : Operation.Data> ApolloClient.registerSuspendingTestResponse(
  operation: Operation<D>,
  data: D? = null,
  errors: List<Error>? = null,
) = registerSuspendingTestResponse(
  operation,
  ApolloResponse.Builder(
    operation = operation,
    requestUuid = uuid4(),
  )
    .data(data)
    .errors(errors)
    .build(),
)

@ApolloExperimental
fun <D : Operation.Data> ApolloClient.registerSuspendingTestNetworkError(operation: Operation<D>): Unit =
  (networkTransport as? TurbineMapTestNetworkTransport)?.registerNetworkError(operation)
    ?: error("Apollo: ApolloClient.registerSuspendingTestNetworkError() can be used only with TurbineMapTestNetworkTransport")
Just changing the map to be a map of turbines seems to be promising so far actually!
m
Yup, sounds like that would do
Maybe you can make it work with plain
Channel
too?
s
Yeah should be possible, but I like the ergonomics and error messages from Turbines anyway. It is just a channel under the hood anyway, with sprinkles on top
๐Ÿ‘ 1
Btw this worked out great! Do you see any use of having this in the library itself too? Have you had other people mention this before? Instantly failing if the response is not in the map makes sense as a default behavior, but with the power of suspend functions it's missing out on this nice functionality of just waiting for a response which can be set a bit later down the line
m
Thinking more about this, isn't what you really want more like a delay in the response (instead of manually triggering the response?)
Delay would model a network more closely, I think?
Re: whether we would include this in the lib, why not. We have many testing APIs (mockserver, queue and map test NetworkTransport) and we're probably due to a cleanup
That cleanup could be an opoprtunity for a "scriptable" test NetworkTransport
s
I don't actually want to care about specific delays or something like that. I want to just assert the state before anything happens. Then assert the state after the request has started but before it came back And then assert it again after it's come back. I do appreciate the power to just do these one by one tbh without having to think about any time-based delays
m
Fair enough ๐Ÿ‘
Out of curiosity, is there a strong reason you're not mocking faking with mock-server?
mock-server requires a local socket and it's doing more work but it's also testing more things closer to a production environment
s
Hmm, I am just used to these testing APIs so I reached for it out of habit I suppose ๐Ÿ˜„ Not sure how testing looks like using by faking the network with mock-server
m
๐Ÿ‘
It's definitely a bit more work (starting/stopping up the server, getting the port bound, etc...)
s
For simplicity's sake I will stick with this since it works for me now, and it closely copies how we do most of our other tests too, but keep the mock-server in mind for if I need something more involved. Thanks for the discussion ๐Ÿ˜Š
๐Ÿ‘ 1
๐Ÿ’™ 1