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:
- 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.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.
Links:
https://square.github.io/retrofit/
https://github.com/square/okhttp/tree/master/mockwebserver