Skip to content

Mock Server Responses in Android Development using Interceptor

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 case, 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.

After thinking 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, it is possible to track not only URL, but also some other parameters. Here is what we ended up with:

data class MockResponse(
    val isEnabled: Boolean,
    val body: String,
    val message: String,
    val serverCode: Int,
    val contentType: String = "application/json"
)
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,
        )
    )
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Protocol
import okhttp3.Response
import 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)
        }
    }

    
}
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()
    }

I will also provide useful interceptors and converters

import okhttp3.Interceptor
import 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.KoinComponent
import 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.ResponseBody
import okhttp3.ResponseBody.Companion.asResponseBody
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.simplexml.SimpleXmlConverterFactory
import 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.Gson
import com.google.gson.reflect.TypeToken
import okhttp3.ResponseBody
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type


// the point here is that if a List comes in, it is converted to a class
class 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.Interceptor
import 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
            }
        }
    }
}

Copyright: Roman Kryvolapov