Mock Server Responses in Android Development using Interceptor
Introduction
I think many developers have encountered a situation when some of the server endpoints are not ready, but it needs to be added to the application. This happens especially often with negative cases like all sorts of errors that need to be processed in the application, and which are often done last.
There are many ways to solve this problem, for example, you can substitute data at different stages of their processing or use OkHttp MockWebServer
https://github.com/square/okhttp/tree/master/mockwebserver
or similar solutions.
Having thought about possible ways, I made an interceptor to change the server response at the earliest possible stage. And it uses a map, in which a simplified model of the server response is located by the key in the form of HttpUrl, then the url is pulled out of the chain, and if it is in the map, the response model is pulled out and the data from it is substituted. I did not add headers, but this can be done very easily, as well as adding as many responses as you like for different urls. Theoretically, you can track not only the url, but also some other parameters. Here's what we ended up with:
MockResponse data class
data class MockResponse( val isEnabled: Boolean, val body: String, val message: String, val serverCode: Int, val contentType: String = "application/json")mockResponses configuration
const val DEBUG_MOCK_INTERCEPTOR_ENABLED = false
val mockResponses = mutableMapOf<String, MockResponse>().apply { put( key = "https://n", value = MockResponse( isEnabled = false, body = "", message = "", serverCode = 200, ) )MockInterceptor class
import okhttp3.Interceptorimport okhttp3.MediaType.Companion.toMediaTypeOrNullimport okhttp3.Protocolimport okhttp3.Responseimport okhttp3.ResponseBody.Companion.toResponseBody
class MockInterceptor : Interceptor {
companion object { private const val TAG = "MockInterceptorTag" }
override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val mockResponse = mockResponses[request.url.toString()] return if (BuildConfig.DEBUG && DEBUG_MOCK_INTERCEPTOR_ENABLED && mockResponse != null && mockResponse.isEnabled ) { logDebug("Intercepted request: ${request.url}", TAG) Response.Builder() .code(mockResponse.serverCode) .message(mockResponse.message) .request(chain.request()) .protocol(Protocol.HTTP_1_0) .body( mockResponse.body .toByteArray() .toResponseBody(mockResponse.contentType.toMediaTypeOrNull()) ) .addHeader("Content-type", "application/json") .build() } else { chain.proceed(request) } }
}OkHttpClient setup
single<OkHttpClient>(named(OKHTTP)) { logDebug("create OkHttpClient)", TAG) OkHttpClient.Builder().apply { val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager { @Throws(CertificateException::class) override fun checkClientTrusted( chain: Array<X509Certificate>, authType: String ) { // NO IMPLEMENTATION }
@Throws(CertificateException::class) override fun checkServerTrusted( chain: Array<X509Certificate>, authType: String ) { // NO IMPLEMENTATION }
override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() } } ) val protocolSSL = "SSL" val sslContext = SSLContext.getInstance(protocolSSL).apply { init(null, trustAllCerts, SecureRandom()) } sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) followRedirects(true) followSslRedirects(true) addInterceptor(get<ContentTypeInterceptor>()) addInterceptor(get<HeaderInterceptor>()) addInterceptor(get<HttpLoggingInterceptor>(named(LOGGING_INTERCEPTOR))) addInterceptor(get<HttpLoggingInterceptor>(named(LOG_TO_FILE_INTERCEPTOR))) if (BuildConfig.DEBUG && DEBUG_MOCK_INTERCEPTOR_ENABLED) { logDebug("add MockInterceptor", TAG) addInterceptor(get<MockInterceptor>()) } connectTimeout(TIMEOUT, TimeUnit.SECONDS) writeTimeout(TIMEOUT, TimeUnit.SECONDS) readTimeout(TIMEOUT, TimeUnit.SECONDS) }.build() }Additional interceptors and converters
I will also provide useful interceptors and converters
import okhttp3.Interceptorimport okhttp3.Response
class ContentTypeInterceptor : Interceptor {
companion object { private const val TAG = "ContentTypeInterceptorTag" }
override fun intercept(chain: Interceptor.Chain): Response { val originalResponse = chain.proceed(chain.request()) val currentContentType = originalResponse.header("Content-Type") val source = originalResponse.body?.source() source?.request(Long.MAX_VALUE) val buffer = source?.buffer val responseBodyString = buffer?.clone()?.readString(Charsets.UTF_8) val newContentType = when { responseBodyString?.trim()?.startsWith("{") == true -> "application/json" responseBodyString?.trim()?.startsWith("<") == true -> "application/xml" else -> null } return if ( newContentType != null && (currentContentType == null || !currentContentType.contains( newContentType, ignoreCase = true )) ) { logDebug("Content type replace with $newContentType", TAG) originalResponse.newBuilder() .header("Content-Type", newContentType) .build() } else { originalResponse } }}import org.koin.core.component.KoinComponentimport org.koin.core.component.inject
class HeaderInterceptor : okhttp3.Interceptor, KoinComponent {
companion object { private const val TAG = "HeaderInterceptorTag" }
private val preferences: PreferencesRepository by inject()
override fun intercept(chain: okhttp3.Interceptor.Chain): okhttp3.Response { logDebug("intercept", TAG) val original = chain.request() val request = original.newBuilder() .header("Content-Type", "application/json") .header("Cookie", "KEYCLOAK_LOCALE=bg") .method(original.method, original.body) val token = preferences.readApplicationInfo()?.accessToken if (!token.isNullOrEmpty()) { logDebug("add token: $token", TAG) request.header("Authorization", "Bearer $token") } return chain.proceed(request.build()) }}import okhttp3.ResponseBodyimport okhttp3.ResponseBody.Companion.asResponseBodyimport org.koin.core.component.KoinComponentimport org.koin.core.component.injectimport retrofit2.Converterimport retrofit2.Retrofitimport retrofit2.converter.gson.GsonConverterFactoryimport retrofit2.converter.simplexml.SimpleXmlConverterFactoryimport java.lang.reflect.Type
class NullOrEmptyConverterFactory : Converter.Factory(), KoinComponent {
companion object { private const val TAG = "NullOrEmptyConverterFactoryTag" }
private val gsonConverterFactory: GsonConverterFactory by inject() private val simpleXmlConverterFactory: SimpleXmlConverterFactory by inject()
override fun responseBodyConverter( type: Type, annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *> { return Converter<ResponseBody, Any?> { body -> if (body.contentLength() != 0L) { val contentType = body.contentType() val source = body.source() source.request(Long.MAX_VALUE) val bufferClone = source.buffer.clone() val subtype = contentType?.subtype val clonedBody = bufferClone.clone().asResponseBody(contentType, bufferClone.size) try { when { subtype?.contains("xml", ignoreCase = true) == true -> { val converter = simpleXmlConverterFactory.responseBodyConverter( type, annotations, retrofit ) converter?.convert(body) }
subtype?.contains("json", ignoreCase = true) == true -> { val converter = gsonConverterFactory.responseBodyConverter( type, annotations, retrofit ) converter?.convert(body) }
else -> { body.string() } } } catch (e: Exception) { logError("Content type not valid, Exception: ${e.message}", e, TAG) clonedBody.string() } } else null } }}import com.google.gson.Gsonimport com.google.gson.reflect.TypeTokenimport okhttp3.ResponseBodyimport org.koin.core.component.KoinComponentimport org.koin.core.component.injectimport retrofit2.Converterimport retrofit2.Retrofitimport java.lang.reflect.Type
// the point here is that if a List comes in, it is converted into a classclass ArrayConverterFactory : Converter.Factory(), KoinComponent {
companion object { private const val TAG = "ArrayConverterFactoryTag" }
val gson: Gson by inject()
override fun responseBodyConverter( type: Type, annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *> { return Converter<ResponseBody, Any?> { body -> when { type is Class<*> && type == SomeResponse::class.java -> { SomeResponse( data = generateList( body = body, type = object : TypeToken<String>() {}.type, ) ) }
else -> { retrofit.nextResponseBodyConverter<Any?>( this@ArrayConverterFactory, type, annotations ).convert(body) } } } }
private fun <T> generateList(body: ResponseBody, type: Type): List<T> { val listType = TypeToken.getParameterized(List::class.java, type).type return gson.fromJson(body.charStream(), listType) }}import okhttp3.Interceptorimport okhttp3.Response
class TempHeaderSavingInterceptor : Interceptor {
companion object { private const val MOCK_STEP_HEADER_FLOW = "X-Flow" private const val MOCK_STEP_HEADER_STEP = "X-State" private var lastRequestHeaderFlow: String? = null private var lastRequestHeaderStep: String? = null }
override fun intercept(chain: Interceptor.Chain): Response { val builder = chain.request().newBuilder() lastRequestHeaderFlow?.let { builder.header(MOCK_STEP_HEADER_FLOW, it) } lastRequestHeaderStep?.let { builder.header(MOCK_STEP_HEADER_STEP, it) } val req = builder.build() return chain.proceed(req).also { response -> response.headers[MOCK_STEP_HEADER_FLOW]?.let { lastRequestHeaderFlow = it } response.headers[MOCK_STEP_HEADER_STEP]?.let { lastRequestHeaderStep = it } } }}