Mock server for mobile testing

During my last project, I’ve been testing a mobile application integrated at the level of business logic with various third-party services. The testing of these services was not part of my task. However, problems with their API blocked the functioning of our application. External APIs issues made it impossible to test our application.

Traditionally, test benches are used to test such applications. But they do not always perform well. As an alternative solution, I used mock servers. I want to talk about them today.

I’ve created a simple REST client for Android to illustrate the approach, which allows sending HTTP requests (GET / POST) to a specific address with the parameters I need. We will test it.

Client application code, dispatchers, and tests can Download from GitLab.

Mocking approaches

  • deploy a mock server in the cloud (or on a remote desktop if we are talking about confidential developments);
  • launch the mock server locally — use the phone on which the mobile application is being tested.

The first option is similar to the test bench. It is possible to allocate a workstation in the network for a mock server and experience the main pitfalls of this approach. For instance: If the remote workstation has died, you stopped responding, something has changed, or you need to monitor the app state and adjust the configuration, i.e., do the same thing with the support of a regular test bench. It’s not very convenient, and these manipulations will undoubtedly take more time than any testing. Thus sometimes, it is more convenient to raise the mock server locally.

Choosing a mock server

  • Mock-server, wiremock — two mock-servers that I couldn’t adequately run on Android. Since all experiments took place as part of a live project, the choice time was limited. After digging for a couple of days, I gave up trying.
  • Restmock is a wrapper over okhttpmockwebserver, which will be discussed in more detail later. It looked good, it started, but its developer hid the ability to set the IP address and port of the mock server. For me, it was critical. Restmock started on some random port. Poking around in the code, I saw that when the server was initialized, the developer used a method that set the port randomly if it did not receive it at the input. Theoretically, one could inherit from this method, but the problem was in the private constructor. As a result, I refused this wrapper.
  • Okhttpmockwebserver — after experimenting with different tools, I’ve chosen the mock server, which got easily assembled and started locally on the device.

Basics

  • The queue of responses. Mock server responses are added to the FIFO queue. It doesn’t matter which API and which path I will be accessing, the Mock-server will throw messages from this queue.
  • The dispatcher allows us to create rules that determine which answer to give. Suppose a request came from a URL containing a path, for example, / get-login /. On this / get-login / mock server provides a single, predefined response.
  • Request Verifier. Based on the previous scenario, I can check the requests that the application sends (that, under the given conditions, a request with specific parameters was received by external server). The answer is unimportant, as it is determined by how the API works. This script implements a Request verifier.

Let’s consider each of the scenarios in more detail.

Response Queue

class QueueTest: BaseTest() {    @Rule
@JvmField
var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java)
@Before
fun initMockServer() {
val mockServer = MockWebServer()
val ip = InetAddress.getByName("127.0.0.1")
val port = 8080
mockServer.enqueue(MockResponse().setBody("1st message"))
mockServer.enqueue(MockResponse().setBody("2nd message"))
mockServer.enqueue(MockResponse().setBody("3rd message"))
mockServer.start(ip, port)
}
@Test
fun queueTest() {
sendGetRequest("http://localhost:8080/getMessage")
assertResponseMessage("1st message")
returnFromResponseActivity()
sendPostRequest("http://localhost:8080/getMessage")
assertResponseMessage("2nd message")
returnFromResponseActivity()
sendGetRequest("http://localhost:8080/getMessage")
assertResponseMessage("3rd message")
returnFromResponseActivity()
}
}

Tests are written with the Espresso framework. In this example, I select request types and send them in a queue. After starting the test, the mock server gives answers in accordance with the prescribed queue, and the test passes without errors.

Implementing a Dispatcher

SimpleDispatcher

class SimpleDispatcher: Dispatcher() {    @Override
override fun dispatch(request: RecordedRequest): MockResponse {
if (request.method == "GET"){
return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request" }""")
} else if (request.method == "POST") {
return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request" }""")
}
return MockResponse().setResponseCode(200)
}
}

The logic in this example is simple: if a GET arrives, I return a message that this is a GET request. If POST, I return a message about the POST request. In other situations, I return an empty request.

Test sources with SimpleDispatcher can be found in the repository.

OtherParamsDispatcher

class OtherParamsDispatcher: Dispatcher() {    @Override
override fun dispatch(request: RecordedRequest): MockResponse {
return when {
request.path.contains("?queryKey=value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request with query parameter queryKey equals value" }""")
request.body.toString().contains("\"bodyKey\":\"value\"") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request with body parameter bodyKey equals value" }""")
request.headers.toString().contains("header: value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was some request with header equals value" }""")
else -> MockResponse().setResponseCode(200).setBody("""{ Wrong response }""")
}
}
}

Firstly, you can pass parameters to the API in the address bar. Therefore, I can put a condition on the path query, for example, “?queryKey=value”.

Secondly, this class allows you to get inside the body of a POST or PUT request. For example, you can use contains, firstly executing toString(). In my example, the condition is triggered when a POST request contains “bodyKey”: ”value”. Similarly, I can validate the request header (header: value).

For examples of tests, I recommend checking out the repository.

ListingDispatcher

При необходимости можно реализовать и более сложную логику — ListingDispatcher. Тем же способом я переопределяю функцию dispatch. Однако теперь прямо в классе задаю дефолтный набор стабов (stubsList) – моков на разные случаи жизни.

class ListingDispatcher: Dispatcher() {
private var stubsList: ArrayList<RequestClass> = defaultRequests()
@Override
override fun dispatch(request: RecordedRequest): MockResponse =
try {
stubsList.first { it.matcher(request.path, request.body.toString()) }.response()
} catch (e: NoSuchElementException) {
Log.e("Unexisting request path =", request.path)
MockResponse().setResponseCode(404)
}
private fun defaultRequests(): ArrayList<RequestClass> {
val allStubs = ArrayList<RequestClass>()
allStubs.add(RequestClass("/get", "queryParam=value", "", """{ "message" : "Request url starts with /get url and contains queryParam=value" }"""))
allStubs.add(RequestClass("/post", "queryParam=value", "", """{ "message" : "Request url starts with /post url and contains queryParam=value" }"""))
allStubs.add(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Request url starts with /post url and body contains bodyParam:value" }"""))
return allStubs
}
fun replaceMockStub(stub: RequestClass) {
val valuesToRemove = ArrayList<RequestClass>()
stubsList.forEach {
if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it)
}
stubsList.removeAll(valuesToRemove)
stubsList.add(stub)
}
fun addMockStub(stub: RequestClass) {
stubsList.add(stub)
}
}

I’ve created an open class RequestClass, all fields of which are empty by default. For this class, I’ve defined a response function that creates a MockResponse object (returning a 200 response or some other responseText), and a matcher function that returns true or false.

open class RequestClass(val path:String = "", val query: String = "", val body:String = "", val responseText: String = "") {    open fun response(code: Int = 200): MockResponse =
MockResponse()
.setResponseCode(code)
.setBody(responseText)
open fun matcher(apiCall: String, apiBody: String): Boolean = apiCall.startsWith(path)&&apiCall.contains(query)&&apiBody.contains(body)
}

As a result, I can build more complex combinations of conditions for stubs. This design seemed to me more flexible, although the principle is very simple.

Most of all, in this approach, I like that I can substitute some stubs in real-time if there is a need to change something in the response of the mock server. When testing large projects, this problem arises quite often, for example, when checking some specific scenarios.

Replacement can be done as follows:

fun replaceMockStub(stub: RequestClass) {
val valuesToRemove = ArrayList<RequestClass>()
stubsList.forEach {
if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it)
}
stubsList.removeAll(valuesToRemove)
stubsList.add(stub)
}

With this dispatcher implementation, the tests remain simple. I also start the mock server but select ListingDispatcher.

class ListingDispatcherTest: BaseTest() {    @Rule
@JvmField
var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java)
private val dispatcher = ListingDispatcher() @Before
fun initMockServer() {
val mockServer = MockWebServer()
val ip = InetAddress.getByName("127.0.0.1")
val port = 8080
mockServer.setDispatcher(dispatcher)
mockServer.start(ip, port)
}
.
.
.
}

For the sake of experiment, I replaced the stub with POST:

@Test
fun postReplacedStubTest() {
val params: HashMap<String, String> = hashMapOf("bodyParam" to "value")
replacePostStub() sendPostRequest("http://localhost:8080/post", params = params)
assertResponseMessage("""{ "message" : "Post request stub has been replaced" }""")
}

To do this, I’ve called the replacePostStub function from a regular dispatcher and added a new response.

private fun replacePostStub() {
dispatcher.replaceMockStub(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Post request stub has been replaced" }"""))
}

In the test above, I verify that the stub has been replaced. Then I add a new stub, which is not default.

@Test
fun getNewStubTest() {
addSomeStub()
sendGetRequest("http://localhost:8080/some_specific_url")
assertResponseMessage("""{ "message" : "U have got specific message" }""")
}
private fun addSomeStub() {
dispatcher.addMockStub(RequestClass("/some_specific_url", "", "", """{ "message" : "U have got specific message" }"""))
}

Request Verifier

When you send a request from a test, it comes to the mock server. Through it, I can access the request parameters using takeRequest ().

@Test
fun requestVerifierTest() {
val params: HashMap<String, String> = hashMapOf("bodyKey" to "value")
val headers: HashMap<String, String> = hashMapOf("header" to "value")
sendPostRequest("http://localhost:8080/post", headers = headers, params = params) val request = mockServer.takeRequest() assertEquals("POST", request.method)
assertEquals("value", request.getHeader("header"))
assertTrue(request.body.toString().contains("\"bodyKey\":\"value\""))
assertTrue(request.path.startsWith("/post"))
}

The same approach can be used for complex JSON requests, including checking the entire structure of the request (you can compare at the JSON level or parse JSON into objects and check equality at the object level).

Summary

Article author: Ruslan Abdulin

PS Subscribe to our social networks: Twitter, Telegram, FB to learn about our publications and Maxilect news.

We are building IT-solutions for the Adtech and Fintech industries. Our clients are SMBs across the Globe (including USA, EU, Australia).