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:

    • Deploy a mock server in the cloud or on a remote machine (if we are talking about confidential developments that cannot be taken to the cloud);
    • launch the mock server locally - directly on the phone on which the mobile application is being tested.

    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:

    • Mock-server , wiremock - two mock servers, which I could not normally run on Android. Since all the experiments took place within the framework of a live project, the time to choose was limited. Picked up with them for a couple of days, I quit trying.
    • Restmock is a wrapper over okhttpmockwebserver , which is described in more detail below. It looked not bad, it started, but the developer of this wrapper hid “under the hood” the possibility of setting the IP address and port of the mock server, but for me it was critical. Restmock started on some random port. Picking 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. In principle, it was possible to inherit from this method, but the problem was in the private constructor. As a result, I refused the wrapper.
    • Okhttpmockwebserver - having tried different tools, I stopped at the mock server, which normally gathered and started locally on the device.

    We understand the principle of work


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

    • The queue of answers . The mock server responses are added to the FIFO queue. No matter which API and which way I will go, the mock server will in turn throw out the messages in this queue.
    • Dispatcher allows you to create rules that determine which answer to give. Suppose a request came at a URL containing a path, for example / get-login /. Therefore, / get-login / the mock server gives a single, predetermined answer.
    • Request Verifier . Based on the previous scenario, I can check the requests that the application sends (that in the given conditions, the request with certain parameters really leaves). In this case, the answer is not important, since it is determined by how the API works. This script implements the Request verifier.

    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.

    classQueueTest: BaseTest() {
        @Rule@JvmFieldvar mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java)
        @BeforefuninitMockServer() {
            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)
        }
        @Testfun 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 a class Dispatcher(). You can inherit from it by redefining the function dispatchin your own way.

    classSimpleDispatcher: Dispatcher() {
        @Overrideoverride fun dispatch(request: RecordedRequest): MockResponse {
            if (request.method == "GET"){
                return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request" }""")
            } elseif (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 appears dispatcher- the 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 SimpleDispatchercan be found in the repository .

    OtherParamsDispatcher


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

    classOtherParamsDispatcher: Dispatcher() {
        @Overrideoverride 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 containspre- run toString(). In my example, the condition is triggered when a POST request comes in that contains “bodyKey”:”value”. Similarly, I can validate the request header ( 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 function dispatch. But now, right in the classroom, I ask the default set of stubs ( stubsList) - mocks for different occasions.

    classListingDispatcher: Dispatcher() {
        privatevar stubsList: ArrayList<RequestClass> = defaultRequests()
        @Overrideoverride 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 a function responsethat creates an object MockResponse(returning the answer 200 or some other responseText), and a function matcherreturning trueor 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.

    classListingDispatcherTest: BaseTest() {
        @Rule@JvmFieldvar mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java)
        privatevaldispatcher= ListingDispatcher()
        @Beforefun 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:

    @Testfun 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 function replacePostStubfrom the usual one dispatcherand added a new one 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.

    @Testfun 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().

    @Testfun 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.

    Also popular now: