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.ResponseBodyimport retrofit2.Callimport retrofit2.http.GETimport retrofit2.http.Queryinterface RemoteApi { @GET("http://some.host/api/data") fun searchByPhrase(@Query("search") searchPhrase: String): Call<ResponseBody>}
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>}
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.ResponseBodyimport org.junit.jupiter.api.Testimport retrofit2.Callimport retrofit2.Retrofitimport retrofit2.http.GETimport retrofit2.http.Queryimport strikt.api.expectThatclass 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 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>}
To handle this case and have request to perform assertions on I came up with the following approach:
- Use OkHttp MockWebServer to record incoming requests
- Wrap system under test invocation with RemoteApi as lambda parameter and RecordedRequest as return type
- Perform assertions on RecordedRequest
import io.reactivex.Singleimport okhttp3.HttpUrlimport okhttp3.OkHttpClientimport okhttp3.ResponseBodyimport okhttp3.mockwebserver.MockResponseimport okhttp3.mockwebserver.MockWebServerimport okhttp3.mockwebserver.RecordedRequestimport org.junit.jupiter.api.Testimport retrofit2.Retrofitimport retrofit2.adapter.rxjava2.RxJava2CallAdapterFactoryimport retrofit2.http.GETimport retrofit2.http.Queryimport strikt.api.expectThatclass 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() } }}
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.
Links:
