Testing Retrofit calls with OkHttp MockWebServer

|

Retrofit – probably the most popular networking client in Android development. Basically it allows to create HTTP client in an interface – you just add annotation with HTTP method, relative or absolute path and proper request is constructed. Retrofit does not generate code in compile time – it creates implementations in runtime.

import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query

interface RemoteApi {
    @GET("http://some.host/api/data")
    fun searchByPhrase(@Query("search") searchPhrase: String): Call<ResponseBody>
}

Example of Retrofit interface

The method invocation:

remoteApi.searchByPhrase("asd")

Will create GET request to:

http://some.host/api/data?search=asd 

Now we will try to come up with a way to check if proper requests are constructed.

Basic test case

We will consider the following Retrofit interface with given configuration:

fun remoteApi(baseUrl: String): RemoteApi {
    return Retrofit.Builder()
            .client(OkHttpClient())
            .baseUrl(baseUrl)
            .build()
            .create(RemoteApi::class.java)
}

interface RemoteApi {
    @GET("/api/data")
    fun searchByPhrase(@Query("search") searchPhrase: String): Call<ResponseBody>
}

Retrofit API with regular Call<T>

How could we actually perform some assertions on that? Was actually GET method used? Was search phrase included in query?

While we are using retrofit2.Call<T> we have Call.request() method available directly and we can perform assertions on Request object:

import okhttp3.ResponseBody
import org.junit.jupiter.api.Test
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Query
import strikt.api.expectThat

class RetrofitTest {
    @Test
    fun `it should GET with query`() {

        val remoteApi = remoteApi(baseUrl = "http://some.api")

        val givenSearchQuery = "given search phrase"

        val call: Call<ResponseBody> = remoteApi.searchByPhrase(givenSearchQuery)

        expectThat(call.request()) {
            assertThat("is GET method") {
                it.method() == "GET"
            }
            assertThat("has given search query") {
                it.url().queryParameterValues("search") == listOf(givenSearchQuery)
            }
        }
    }
}

Test case – assertions on call.request()

Test case with non-standard call adapter factory

In my opinion the power of Retrofit comes from call and converter adapter factories – instead of relying on built-in types (such as retrofit2.Call) you can add RxJava call adapter and framework would find a way to convert Call<T> into rx Single<T>. Unfortunately we won’t have direct access to Call.request() method then!

fun remoteApi(baseUrl: HttpUrl): RemoteApi {
    return Retrofit.Builder()
            .client(OkHttpClient())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl(baseUrl)
            .build()
            .create(RemoteApi::class.java)
}

interface RemoteApi {
    @GET("/api/data")
    fun searchByPhrase(@Query("search") searchPhrase: String): Single<ResponseBody>
}

Retrofit API with RxJava2 call adapter factory

To handle this case and have request to perform assertions on I came up with the following approach:

  1. Use OkHttp MockWebServer to record incoming requests
  2. Wrap system under test invocation with RemoteApi as lambda parameter and RecordedRequest as return type
  3. Perform assertions on RecordedRequest
import io.reactivex.Single
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.jupiter.api.Test
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import strikt.api.expectThat

class RetrofitTest {
    @Test
    fun `it should GET with query`() {

        val givenSearchQuery = "given search phrase"

        val request: RecordedRequest = takeMockRequest {
            searchByPhrase(givenSearchQuery)
                    .subscribe()
        }

        expectThat(request) {
            assertThat("is GET method") {
                it.method == "GET"
            }
            assertThat("has given search query") {
                it.requestUrl.queryParameterValues("search") == listOf(givenSearchQuery)
            }
        }
    }

    private fun takeMockRequest(sut: RemoteApi.() -> Unit): RecordedRequest {
        return MockWebServer()
                .use {
                    it.enqueue(MockResponse())
                    it.start()
                    val url = it.url("/")

                    sut(remoteApi(url))

                    it.takeRequest()
                }
    }
}

Test case with RecordedRequest

I highly encourage you to explore more methods and properties available in RecorderRequest – you may find there more things that may be useful in your test.


Now we have a way to perform assertions on request that was recorded in MockWebServer – with this approach we can design even more complex integration test suites.

https://square.github.io/retrofit/

Retrofit website

https://github.com/square/okhttp/tree/master/mockwebserver

MockWebServer

Join AndroidPro newsletter & get roadmap for Android devs

👉 Architecture
👉 CI/CD
👉 Testing
👉 Compose
👉 projects to learn from
… in a single document


Read more:

  • #kotlinDevChallenge – 10

    #kotlinDevChallenge – 10

    Implement a function that sums the first 5 even numbers from each of three given integer Flows and returns their total sum. Properly handle…

  • #kotlinDevChallenge – 9

    #kotlinDevChallenge – 9

    Write a function that takes a list of lists of integers and returns a single list containing all unique elements. Ensure that the resulting…

  • #kotlinDevChallenge – 8

    #kotlinDevChallenge – 8

    data class A is given. We create an instance of this class and then call the .copy() function. What will be printed out? Mark…