Unit Test of Retrofit by MockWebServer

With customized Retrofic Converter.Factory, Gson JsonAdapter, we lack unit tests for Retrofit methods to make sure that the responses will be converted to correct structures.

Bram Yeh
3 min readJun 14, 2019

In our projects, we use lots of customized Converter.Factory into Retrofit, and customized JsonAdapter for these converters. It looks likes

// There are too many conveters that might affect our data models
val apiService = Retrofit.Builder()
.baseUrl(API_SERVER_URL)
.addConverterFactory(GraphQLConverterFactory())
.addConverterFactory(AppConverterFactory())
.addCallAdapterFactory(CallAdapterFactory())
.client(AppOkHttpClient())
.build()
.create(ApiService::class.java);
// And we also have customize Gson builder ... for converter
val builder = GsonBuilder()
.registerTypeAdapter(Date::class.java, DateConverter())
.registerTypeAdapterFactory(EnumTypeAdapterFactory())

We want to make sure the responses will be converted to correct structures, after adding more converters and type adapters. So it’s good to use MockWebServer inside the unit tests for our ApiServer,

interface ApiService {
@GET("/product")
fun getProduct(@Query("id") id: String): Single<Product>
}.

Use MockWebServer with Retrofit for Unit Test

In the beginning, add the dependence of Mockwebserver,

testImplementation "com.squareup.okhttp3:mockwebserver:$okHttpVersion"

Now we can start to implement our tests.

First, we should initial MockWebServer and use its URL as our server URL, Retrofit.Builder().baseUrl(mockWebServer.url(“/”)), to create our mock ApiService (and we should build with the same customized converters and client).

Then we prepare MockReponse with a HttpURLConnection.HTTP_OK status code and set a custom body. (You can get more details here)

After scheduling the responses, mockWebServer.enqueue(response), just call the retrofit methods as a general case, apiService.getProduct(“fake id”), to obtain the data structure and verify.

class APIServiceTest {    private var mockWebServer = MockWebServer()    private lateinit var apiService: ApiService    @Before
fun setup() {
mockWebServer.start()
apiService = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GraphQLConverterFactory())
.addConverterFactory(AppConverterFactory())
.addCallAdapterFactory(CallAdapterFactory())
.client(AppOkHttpClient())
.build()
.create(ApiService::class.java)
}
@After
fun teardown() {
mockWebServer.shutdown()
}
@Test
fun testAppVersions() {
// Assign
val response = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(readContentFromFilePath())
mockWebServer.enqueue(response)// Act
val product = apiService.getProduct("101").blockingGet()
// Assert
....
}

Besides, we can use Dispatcher instead.

val dispatcher = object : Dispatcher() {
@Throws(InterruptedException::class)
override fun dispatch(request: RecordedRequest): MockResponse {
return when (request.path) {
"/product" -> {
val response = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(readContentFromFilePath())
}
// TODO, if you need other unit test for other methods
else -> MockResponse().setResponseCode(404)
}
}
}
mockWebServer.setDispatcher(dispatcher)
mockWebServer.start()

Troubleshooting for “unable to determine cleartext support.”

Generally, we should be happy to see our unit tests pass; however, when running tests with Roboelectric, OkHttp crashes and throws an exception.

java.lang.AssertionError: unable to determine cleartext support

Many people said we could work around by setting @Config(sdk = 23) on each test. I did it but failed. (If there are someones that do it successfully, please tell me, I want to know why I failed.)

To work around this issue, I have to shadow android.security.NetworkSecurityPolicy to override isCleartextTrafficPermitted(), for each test that involved OkHttp.

NetworkSecurityPolicyShadow Implements NetworkSecurityPolicy, indicates that it is intended to shadow this Android class declaration, so Robolectric runtime searches with this Implementation annotation and calls NetworkSecurityPolicyShadow in place of the methods on the NetworkSecurityPolicy class.

@Implements(NetworkSecurityPolicy::class)
class NetworkSecurityPolicyShadow {
@Implementation
fun isCleartextTrafficPermitted(host: String): Boolean {
return true
}
companion object {
@JvmStatic
val instance: NetworkSecurityPolicy
@Implementation
get() {
try {
val shadow = Class.forName("android.security.NetworkSecurityPolicy")
return shadow.newInstance() as NetworkSecurityPolicy

} catch (e: Exception) {
throw AssertionError()
}
}
}
}

In each test, define config to use this shadows,

@Config(shadows = [NetworkSecurityPolicyShadow::class])

If you use Java, here is Java version of NetworkSecurityPolicyShadow,

@Implements(NetworkSecurityPolicy.class)
public static class NetworkSecurityPolicyShadow {
@Implementation
public static NetworkSecurityPolicy getInstance() {
try {
Class<?> shadow = Class.forName("android.security.NetworkSecurityPolicy");
return (NetworkSecurityPolicy) shadow.newInstance();
} catch (Exception e) {
throw new AssertionError();
}
}
@Implementation
public boolean isCleartextTrafficPermitted(String host) {
return true;
}
}

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Bram Yeh
Bram Yeh

Written by Bram Yeh

Lead Android & iOS Mobile Engineer at Yahoo (Verizon Media) Taiwan https://www.linkedin.com/in/hanruyeh/

Responses (3)

Write a response