📜 ⬆️ ⬇️

Mock server to automate mobile testing

While working on the latest project, I ran into testing a mobile application connected at the business logic level with various third-party services. Testing these services was not part of my task, but problems with their API blocked the work on the application itself - tests did not fall due to problems inside, but because of the inability of the API, not even reaching the required functionality.

Traditionally, for testing such applications, stands are used. But they do not always work normally, and this interferes with work. As an alternative solution, I used moki. About this thorny path and I want to tell you today.

image

In order not to touch the code of a real project (under NDA), for clarity of the further presentation, I created a simple REST client for Android that allows you to send HTTP requests (GET / POST) to a certain address with the parameters I need. We will test it.
The application client code, dispatchers, and tests can be downloaded from GitLab .
')

What are the options?


In my case, there were two approaches to mopping:


The first option is slightly different from the test bench. Indeed, it is possible to allocate a workplace in the network for the mock server, but it will need to be maintained, like any test facility. This is where we will face the main pitfalls of this approach. The remote workplace has died, has stopped responding, something has changed - it is necessary to monitor, change the configuration, i.e. do everything the same as with the support of a regular test bench. We do not fix the situation for ourselves in any way, and it will definitely take more time than any local manipulations. So specifically in my project it was more convenient to raise the mock server locally.

Choosing a mock server


There are many different tools. I tried to work with several and almost in each I encountered certain problems:


We understand the principle of work


The current version of okhttpmockwebserver allows you to implement several scenarios:


Consider each of the scenarios in more detail.

Response queue


The simplest implementation of the mock server is the response queue. Prior to the test, I define the address and port where the mock server will be deployed, as well as the fact that it will work according to the message queuing principle - FIFO (first in first out).

Next, run the mock server.

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 using the Espresso framework, designed to perform actions in mobile applications. In this example, I select the types of requests and send them in turn.
After starting the test, the mock server gives it answers in accordance with the prescribed queue, and the test passes without errors.

Dispatcher implementation


Dispatcher is a set of rules by which the mock server operates. For convenience, I created three different dispatchers: SimpleDispatcher, OtherParamsDispatcher and ListingDispatcher.

Simpledispatcher


To implement the dispatcher, okhttpmockwebserver provides the Dispatcher() class. You can inherit from it by redefining the dispatch function in your own way.

 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 GET arrives, I return a message that this is a GET request. If POST, return message about POST request. In other situations, return an empty request.

In the test, a dispatcher appears - an object of the class SimpleDispatcher , which I described above. Further, as in the previous example, the mock server is started, only this time a kind of rule for working with this mock server is indicated - the same dispatcher.

Test sources with SimpleDispatcher can be found in the repository .

OtherParamsDispatcher


By overriding the dispatch function, I can push off from other request parameters to send responses:

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

In this case, I demonstrate several options for the conditions.

First, the API can pass parameters in the address bar. Therefore, I can put a condition on the entry in the path of any ligament, for example “?queryKey=value” .
Secondly, this class allows you to climb inside the body (body) of POST or PUT requests. For example, you can use contains by first executing toString() . In my example, the condition is triggered when a POST request comes in, containing the “bodyKey”:”value” . Similarly, I can validate the request header : value ( header : value ).

For examples of tests I recommend to contact the repository .

ListingDispatcher


If necessary, you can implement a more complex logic - ListingDispatcher. In the same way, I override the dispatch function. But now, right in the class, I set the default set of stubsList ( stubsList ) - mocks for different occasions.

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

For this, I created an open class RequestClass , all fields of which are empty by default. For this class, I set the response function, which 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 construction seemed to me more flexible, although the principle at its basis is very simple.

But most of all in this approach, I liked that I could inject some stubs on the go if there was a need to change something in the answer of the mock server on one test. When testing large projects, such a task occurs quite often, for example, when testing 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 implementation of the dispatcher tests remain simple. I also start the mock server, just choose 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" }""") } 

For this, I called the replacePostStub function from the normal 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 check that the stub has been replaced.
Then I added a new stub, which was not in 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


The last case - Request verifier - provides not mocking, but checking requests sent by the application. To do this, I just start the mock server by implementing the dispatcher so that the application returns at least something.
When sending a request from the test, he 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")) } 

Above, I showed a check on a simple example. Exactly the same approach can be used for complex JSON, including checking the entire structure of the request (you can compare at the level of JSON or parse JSON on objects and check equality at the level of objects).

Results


In general, I liked the tool (okhttpmockwebserver), and I use it on a large project. Of course, there are some little things that I would like to change.
For example, I do not like having to knock on the local address (localhost: 8080 in our example) in the configs of my application; Perhaps I will still find a way to configure everything so that the mock server responds when trying to send a request to any address.
Also, I lack the ability to forward requests - when the mock server sends the request further, if it does not have a suitable stub for it. In this mock server there is no such approach. However, they did not even reach their implementation, since at the moment in a “combat” project it is not worth such a task.

Article author: Ruslan Abdulin

PS We publish our articles on several sites Runet. Subscribe to our pages on the VK , FB or Telegram channel to learn about all of our publications and other news from Maxilect.

Source: https://habr.com/ru/post/430530/


All Articles