Our world is more and more connected, and the apps we use on our phones are a good demonstration of this. Most of the apps we run on our smartphones rely on remote calls to provide great services to our users. The problem is that even if our wireless connections have improved and our bandwidth increased, we still regularly face situations where the network fails us: airplane mode, remote countryside area with poor coverage or even the infamous tunnel that unexpectedly ends your annoying calls.

So what happens when your app loses its ability to contact a remote server? Does it display an error message or an infinite loader screen? Does it freeze or, even worse, crash? Those cases are often considered corner cases when developing a new feature, but even in 2019, loss of connectivity is still a thing. What about users that would like to see what your app is like but do not have an account yet with interesting data to showcase the great work you’ve done? Or your developers that started implementing features before the services were ready and get stuck on most of their tasks?

Well, all of those issues are not necessarily easy to address, and probably not with a single silver bullet library, but for some of those situations, a simple workaround exists: mocking your HTTP connections. For development purposes, you could set up a fake server responding with predefined mock scenarios. It’s not that hard to implement and can give front-end developers some breathing space to keep coding while they wait for the actual servers to come live.

But this does not address cases where you’re using the app on a phone that does not have access to any server at all, not even your mock server. I’ve seen the case in the past when my team and I tried to demonstrate our work at the end of a sprint, only to find out we were stuck in a room with no actual access to the company’s private network. In those cases, you need a solution that can be packaged inside the app, so the device can be self-sufficient, at least until your demo is done.

The solution had to be decoupled from our business logic so we could easily switch from actual connected data to mocked state, and not bloat our code with “if” statements to load fake data whenever the network was not accessible. Since our project, like most of Android apps today, use the famous OkHttp stack for HTTP calls, it seemed like a possible entry point to insert that logic.

That’s how I ended up crafting a solution based on OkHttp’s Interceptors. And bundled that solution into an open-source library that I just released: HttpMocker. The principle is rather simple: by adding an interceptor, you can block network calls and provide fake responses without altering your business logic. From the point of view of the rest of your app, your code will never know whether the network call was actually executed or whether it was mocked.

Quickstart

So how do we use that library? HttpMocker is a lightweight library written in Kotlin that provides an OkHttp interceptor to stop network calls and return a canned response from a scenario file or an in-memory rule. All you need to set it up is to add a MockResponseInterceptor to your OkHttp client. Here is an example with minimal configuration using dynamic mocks:

val interceptor = MockResponseInterceptor.Builder()

.useDynamicMocks{

ResponseDescriptor(code = 200, body = "Fake response body")

}

.setInterceptorStatus(ENABLED)

.build() val client = OkHttpClient.Builder()

.addInterceptor(interceptor)

.build()

If your interceptor is disabled, it will not interfere with actual network calls. If it is enabled, it will need to find scenarios to mock the HTTP calls. Dynamic mocks imply that you have to provide the response for each request programmatically, which allows you to define stateful responses (identical calls could lead to different answers based on what the user did in between these calls):

val callback = object : RequestCallback {

var count = 0 override fun loadResponse(request: Request) =

ResponseDescriptor(body = "Fake response body ${count++}")

}

}

You can compute the response by implementing the RequestCallback interface or simply provide a lambda function to do the computation and return the simulated response as a ResponseDescriptor (that is a simple data class describing all the details of the answer: HTTP response code, headers, body, etc.).

Another option is to use static mocks. Static mocks are scenarios stored as static files. Here is an example for an Android app using static mocks:

val interceptor = MockResponseInterceptor.Builder()

.parseScenariosWith(mapper)

.loadFileWith { context.assets.open(it) }

.setInterceptorStatus(ENABLED)

.build()

In this example, we decided to store the scenarios in the assets folder of the app, so we provided a lambda function using context.assets.open() to load the files. If you’re not in an Android app, you could also have them as resources in your classpath and use the Classloader to access them or even store them in a certain folder and access that folder with any File API you’re comfortable with.

Additionally, you need to provide a Mapper to parse the JSON scenario files. In theory, scenarios do not have to be stored as JSON: you could use XML if you prefer, or any other format, as long as you provide your own Mapper class to serialize and deserialize the business objects.

val mapper = object : Mapper {

override fun deserialize(payload: String): List<Matcher> = ...

override fun serialize(matchers: List<Matcher>): String = ...

}

As far as this lib is concerned though, a few mappers are available out of the box, but they only handle JSON format for the moment and are based on Jackson, Gson, Moshi and Kotlinx serialization. They are provided in specific modules so you can choose one based on the JSON library you already use, thus limiting the risk for duplicate libraries serving the same purpose in your app. An implementation based on a custom JSON parser that does not use any dependencies is also available.

On top of those mandatory settings, there are a few optional ones available that can be tweaked.

A FilePolicy defines which file to check to find a match for a request. The default option mirrors the URL path (your scenarios will have to be stored in folders that match the URL they address). A few other policies are provided in the library, but you can also define your own.

val interceptor = MockResponseInterceptor.Builder()

.parseScenariosWith(mapper)

.decodeScenarioPathWith(filingPolicy)

.loadFileWith { context.assets.open(it) }

.setInterceptorStatus(ENABLED)

.build()

A fake network delay can also be defined: since mocked responses are much faster than actual HTTP calls, you might want to include some arbitrary delay when processing them in order to maintain the original behavior of your app. You could use this parameter to ensure your loading screens show up and are not skipped for instance. This parameter is a general setting, but it can be overridden on a per request basis in the scenarios.

val interceptor = MockResponseInterceptor.Builder()

.parseScenariosWith(mapper)

.loadFileWith { context.assets.open(it) }

.setInterceptorStatus(ENABLED)

.addFakeNetworkDelay(300L)

.build()

Finally, this interceptor supports a few different modes: disabled, enabled, mixed or record. When disabled, it will not interfere with actual network calls and let them go through. When enabled, it will stop all requests and answer them based on predefined scenarios. If you choose the mixed mode, requests that can not be answered by a predefined scenario will actually be executed. Hence the name: responses can come from a scenario file or from an actual HTTP call. Several interceptors could even be stacked to handle different cases (see the tests for an example).

Last but not least, the interceptor also has a recording mode. This mode allows you to record scenarios without interfering with your request. If you choose this mode to produce your scenarios, you will have to provide a root folder where the scenarios should be stored.

val interceptor = MockResponseInterceptor.Builder()

.parseScenariosWith(mapper)

.loadFileWith { context.assets.open(it) }

.setInterceptorStatus(RECORD)

.saveScenariosIn(File(rootFolder))

.build()

Also, you should realize that all request and response attributes will be recorded. Generally, you will want to review the resulting scenarios and clean them up a bit manually. For instance, each request will be recorded with its HTTP method, its path, each header or parameter, its body. If you’re recording calls to use them as future scenarios, all those details might not be very important to you: maybe all you care about is the URL and method, in which case, you can delete all the superfluous criteria manually. On the other hand, using a MockResponseInterceptor in record mode with a SingleFilePolicy could be handy to log all network calls and responses in a file so you can extract them for debugging purposes later (only during development and tests though, don’t do that in production!).

Building static scenarios

Answering a request with a static mock is done in two steps:

First, if the interceptor is enabled (or in mixed mode), it will try to compute a file name were the appropriate scenario should be stored. Based on the filing policy you choose, those files can be organized in a lot of different ways: all in the same folder, in separate folders matching the URL path, ignoring or not the server host name.

Second, once the file is found, its content will be loaded, and a more exact match will have to be found. Scenario files contain a list of “ Matchers ”, that is a list of request patterns and corresponding responses. Based on the request it is trying to answer, the interceptor is going to scan through all the request declarations and stop as soon as it finds one that matches the situation.

When writing a request pattern, the elements included are supposed to be found in the requests to match. The more elements, the more precise the match has to be. The less elements, the more permissive the match. A request can even be omitted altogether (in this case, all requests match). For instance:

When specifying a method, matching request have to use the same HTTP method.

When specifying query parameters, matching requests must have at least all these parameters (but can have more).

all these parameters (but can have more). When specifying headers, matching request must have at least all the same headers (but can have more).

Here is an example of scenario in JSON form:

[

{

"request": {

"method": "post",

"headers": {

"myHeader": "myHeaderValue"

},

"params": {

"myParam": "myParamValue"

},

"body": ".*1.*"

},

"response": {

"delay": 50,

"code": 200,

"media-type": "application/json",

"headers": {

"myHeader": "headerValue1"

},

"body-file": "body_content.txt"

}

}, {

"response": {

"delay": 50,

"code": 200,

"media-type": "application/json",

"headers": {

"myHeader": "headerValue2"

},

"body": "No body here"

}

}

]

In this example, a POST request on the corresponding URL, including a query param myParam with the value myParamValue , a header myHeader with the value myHeaderValue and a body containing the digit ‘1’ (based on the regex used as body) will match the first case: it will be answered a HTTP 200 response of type application/json , with a header myHeader of value headerValue1 . The body for this response will be found in a nearby file name body_content.txt . In any other cases, the request will be answered with the second response: a HTTP 200 response with headerValue2 as header and a simple string No body here as body.

That’s it

With just a few lines of code, this library allows you to intercept network calls and provide mock responses. As developers, our team loves to have an offline option to run the app during development or for demonstrations.

So check out the demo app or give it a try for yourself and let me know what you think!

HttpMocker: https://github.com/speekha/httpmocker