Create a Smart Mock with Minimum Effort

Dmitriy Inshakov
Exness Tech Blog
Published in
10 min readFeb 9, 2023

--

Your test environment may not always be ready for isolation from external services. Maybe the test framework isn’t ready either. But as you come to realize this needs to change, your search for a seamless transition to mocks begins. In this article I share some approaches to mocking web services and combining them with test automation and share my experience of adding MockServer to a company’s tool stack.

Test Environment Evolution

Throughout my time working as an automation specialist in different companies, I’ve noticed a typical evolution of test environments. It usually starts with a development bench, then a release test bench, followed by the growing demand for more test servers among collaborating teams who then set up their own benches, and so on. After a while, this network becomes quite difficult to operate.

At some point, your test run stability becomes strongly affected by another team’s test bench, so you want to do your best to isolate your tests from third-party services. Moreover, automating some tests may require you to prepare appropriate responses from these services, which is not always possible. For example, you need to check an app’s response to error 500 from an external service but cannot make the third-party server return this error. Automation specialists and developers then resort to spoofing (mocking) third-party requests to ensure the isolation and flexibility they need for testing and development.

Service mocking scenario
Service mocking scenario

What Can You Do with Existing Tests?

If your company’s test environment has evolved according to the scenario illustrated above, the test-rich framework is likely already developed and heavily depends on the existing test infrastructure and its connectivity to external services.

The decision to isolate its test infrastructure raises the question of how to make the tests play nice with mock services. I’d highlight two ways of setting up the interconnection between tests and mocks:

  1. Use a request mocking tool that responds to a request using a special preset rule. There are numerous such tools, including MockServer.
  2. Use your proprietary service that almost completely mocks an external service at the desired interaction points.

Both of these options have the right to exist and can be combined in one project. They both have advantages and disadvantages. Let’s take a closer look at.

The test prepares all the required mock data before performing them.

To do this, you will need to deploy a mocking tool in your test environment and redirect the tested services to it.

Then set up all necessary emulated data generators in the test framework, develop a client for this service, and assign the methods that generate responses in the mock service to the appropriate tests and their steps.

For example, you need to develop a user creation test that loads data from third-party information and addresses the system or microservice. In other words, you need to emulate this third-party service. The final result may look as follows in pseudo-code:

# Create a user by loading data from a single third-party service

User = DataGenerator.generateNewUser()
MockClient.userHandler.setNewExpectationForExternalSystem(User)
Response = ServiceClient.createUser(User)
Assert Response.message == "User created"

This option allows you to describe the entire behavior logic in each test — both for the tested system and third-party services. It allows you to clearly see and set the data to be returned by these services. This improves test visibility and makes creating preconditions more flexible. However, there are downsides. In order to emulate a more complex service than the one shown in the example above, you will need to pre-generate expectations in multiple services and synchronize the data between them correctly. This makes the precondition creation process more complicated and increases the test size. For example:

# Create a user by loading data from multiple services

User = DataGenerator.generateNewUser()
MockClient.userHandler.setExpectationForExternalSystem(User)
MockClient.userHandler.setExpectationForFNS(User.id)
MockClient.userHandler.setExpectationForBilling(User.id, User.name, User.email)
MockClient.userHandler.setExpectationForBackOffice(User.id, User.accounts[0].amount)
Response = ServiceClient.createUser(User)
Assert Response.message == "User created"

There’s one more caveat: if another team using a different development language wants to apply your service mocking practices, they won’t be able just to use your library as-is to work with the expectations generator, and will have to reinvent the wheel.

Smart Mocks. Emulate the Service at the Points You Need

In this case, you need to repeat the emulated system’s logic in an auxiliary and often proprietary service. This demands investing extra time into creating and supporting this service but doesn’t require any special changes to the test framework. On top of that, a similar mocking service can be used by other teams.

But if you want to add special logic required by the tests, you will have to make changes to the service code, reassemble and deploy it, etc., which may be inconvenient.

Both Options Combined

Our team wanted to get rid of the existing patchwork mess and improve our test flexibility. We needed to combine all the advantages of the aforementioned options in a single tool that would be easier to support, could withstand the load, and allow all participants to easily mock the needed requests. MockServer ticks all these boxes.

If a tester needs to generate an expected response to any request (expectation), they use the convenient MockServer API:

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
"httpRequest": {
"path": "/service/path/"
},
"httpResponse": {
"body": "response_body",
}
}'

In the example above, the following rule is created: return response 200 with “response_body” to the GET request to /service/path. I can create this expectation at a specific moment during the test, giving the tested system the response I want. It is really handy when you test special data sets, error codes, etc., i. e. anything that is difficult or impossible to emulate using a working service.

Moreover, MockServer can create dynamic callbacks that allow for desired code execution during request-response generation. In other words, you can both generate a response dynamically (for example, by creating a random ID) and send a request to a database, another service, etc., i. e. execute any logic. I can even create a new expectation in MockServer! It allows us to create a smart mock on a ready-to-use platform with minimal code to describe the necessary part of the emulated logic.

Let’s assume you have been tasked with creating a service that emulates interaction with a user management system. The solution concept could look as follows:

  1. Each test is atomic and sends the POST /api/v1/user request to the system to create a new user, while the user parameters are passed to the request payload. In addition, an account is created and attached to the user.
  2. This POST request will be responded to by executing any code you need, such as processing the passed user parameters, creating a unique user ID, and generating the relevant responses to MockServer for future requests from the tested system.
  3. Preparing expectations (the required response) for future GET requests for /api/v1/users/{user_uid} and /api/v1/users/{user_uid}/accounts/{account_id}

Let’s take a closer look at how this can be done.

Kotlin Implementation Example

As you start MockServer, you can prompt it to include your project’s assembled JAR files in its classpath, which will allow you to use your code. Here’s a step-by-step example of creating the UserHandleExpectation class, i. e. the future smart mock:

  • Add dependencies to pom.xml
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-client-java-no-dependencies</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty-no-dependencies</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
  • UserHandleExpectation.kt
package userApi

import com.google.gson.Gson
import org.mockserver.client.MockServerClient
import userApi.dto.*
import org.mockserver.matchers.TimeToLive
import org.mockserver.matchers.Times
import org.mockserver.mock.action.ExpectationResponseCallback
import org.mockserver.model.*
import org.mockserver.model.JsonBody.json
import utils.getIsoCurrentDate
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue

const val TTL_SEC: Long = 600

class UserHandleExpectation : ExpectationResponseCallback {
override fun handle(httpRequest: HttpRequest): HttpResponse {
val gson = Gson()
val mockServerClient = MockServerClient("localhost", 1080)

// Convert POST payload to data structure
val userPayload: UserPOSTRequestDTO? =
gson.fromJson(httpRequest.bodyAsJsonOrXmlString, UserPOSTRequestDTO::class.java)

// Create expectation for GET /users/{userId}
val userUUID = UUID.randomUUID().toString()
val userGETResponse = UserGETResponseDTO(
userUid = userUUID,
name = userPayload?.name,
surname = userPayload?.surname,
currency = userPayload?.currency,
region = userPayload?.region,
serverCode = userPayload?.server_code,
createdDate = getIsoCurrentDate()
)
mockServerClient.`when`(
HttpRequest.request()
.withMethod("GET")
.withPath("/api/v1/users/${userUUID}"),
Times.unlimited(), TimeToLive.exactly(TimeUnit.SECONDS, TTL_SEC)
).respond(
HttpResponse.response()
.withContentType(MediaType.APPLICATION_JSON)
.withBody(json(gson.toJson(userGETResponse, UserGETResponseDTO::class.java)))
)

// Create expectation for GET /users/{userId}/accounts/{accountId}
val accountId = userUUID.hashCode().absoluteValue
val accountGETResponse = AccountGETResponseDTO(
id = accountId,
userUid = userUUID,
currency = userPayload?.currency,
status = "ACTIVE",
expired = false
)
mockServerClient.`when`(
HttpRequest.request()
.withMethod("GET")
.withPath("/api/v1/users/${userUUID}/accounts/${accountId}"),
Times.unlimited(), TimeToLive.exactly(TimeUnit.SECONDS, TTL_SEC)
).respond(
HttpResponse.response()
.withContentType(MediaType.APPLICATION_JSON)
.withBody(json(gson.toJson(accountGETResponse, AccountGETResponseDTO::class.java)))
)

// Prepare response for current callback
val userPOSTCallbackResponse = UserPOSTResponseDTO(
userId = userUUID,
name = userPayload?.name,
surname = userPayload?.surname,
account = accountId,
serverCode = userPayload?.server_code,
region = userPayload?.region
)

return HttpResponse.response()
.withStatusCode(HttpStatusCode.CREATED_201.code())
.withContentType(MediaType.APPLICATION_JSON)
.withBody(json(gson.toJson(userPOSTCallbackResponse, UserPOSTResponseDTO::class.java)))
}
}
  • Start MockServer

Assemble the project and start MockServer using a docker with the following command:

docker run -d - rm -v <path to your folder with JAR files>:/libs -p 1080:1080 - name mock mockserver/mockserver -serverPort 1080

Here, the /libs folder mount must contain all the project’s compiled JAR files.

In order to get the entry point when the mock responds (POST /api/v1/users request in this case), you need to create an expectation in MockServer. Run the command:

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
"httpRequest": {
"path": "/api/v1/users",
"method": "POST"
},
"httpResponseClassCallback": {
"callbackClass": "userApi.UserHandleExpectation"
}
}'

When you browse the MockServer dashboard at http://localhost:1080/mockserver/dashboard, you will see the following.

MockServer dashboard with the main expectation created

Send a POST request to generate the UUID for the user and their account, as well as to create responses for future GET requests:

curl -v "http://localhost:1080/api/v1/users" -d'{
"name": "John",
"surname": "Doe",
"currency": "USD",
"region": "CA",
"server_code": "USA"
}'

Response:

{
"userId": "2182884c-89f1–4a74-b180-c73848f8d8ad",
"name": "John",
"surname": "Doe",
"account": 1492317915,
"serverCode": "USA",
"region": "CA"
}

You can see the new expectations on the dashboard:

GET-generated expectations with automatically generated IDs

Thus, by developing this concept you can create a MockServer-based smart mock that will generate the required responses to all the endpoints you need based on your input.

Challenges and Finetuning of Settings

After implementing this type of mock, we decide to run a load test to see how quickly it can process dynamic callbacks and create expectations. But when we need to create multiple expectations while processing a single callback, MockServer sometimes fails with 404 after a 20-second timeout.

First, we finetune some settings as follows:

mockserver.logLevel=INFO
mockserver.maxExpectations=12000
mockserver.watchInitializationJson=false
mockserver.maxLogEntries=100
mockserver.outputMemoryUsageCsv=false
mockserver.maxWebSocketExpectations=2000
mockserver.disableSystemOut=false
mockserver.nioEventLoopThreadCount=100
mockserver.clientNioEventLoopThreadCount=100
mockserver.matchersFailFast=true
mockserver.alwaysCloseSocketConnections=true
mockserver.webSocketClientEventLoopThreadCount=100
mockserver.actionHandlerThreadCount=100

Make sure that MockServer has enough memory and CPU. It’s best to have about 1 GB of memory and multiple CPU cores if you plan to run tests in more than 16 parallel threads and actively use dynamic callbacks.

Of all the settings, mockserver.matchersFailFast=true helps best. This setting responds with a mismatch at the first failed expectation. In our case, since we only compare by path, it’s of no critical importance.

Finetuning improves the situation but doesn’t solve the problems completely. Upon failing to find a solution, we start using MockServer’s HTTP client instead of the native Java client as shown in the example above. Moreover, MockServer supports a batch generation of expectations in a single request only via JSON REST API, which has proven useful and convenient in reducing the load.

  • Add the okktp library to the project’s pom.xml
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version>
</dependency>
  • Create a separate HTTP client class

MockClient.kt

package client

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.mockserver.mock.Expectation
import java.net.URL
import java.util.concurrent.TimeUnit


class MockClient {

private val mockBaseURL = "http://localhost:1080/mockserver/expectation"

private fun getHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
builder.connectTimeout(30, TimeUnit.SECONDS)
builder.readTimeout(30, TimeUnit.SECONDS)
builder.writeTimeout(30, TimeUnit.SECONDS)
return builder.build()
}

fun setExpectations(expectation: List<Expectation>) {
val url = URL(mockBaseURL)
val client = getHttpClient()
val mediaType = "application/json; charset=utf-8".toMediaType()
val body = expectation.toString().toRequestBody(mediaType)
val request = Request.Builder().url(url).put(body).build()
client.newCall(request).execute().close()
}
}

With this client, creating expectations in the callback class code appears as follows:

UserHandleExpectation.kt

package userApi

import client.MockClient
import com.google.gson.Gson
import userApi.dto.*
import org.mockserver.matchers.TimeToLive
import org.mockserver.matchers.Times
import org.mockserver.mock.action.ExpectationResponseCallback
import org.mockserver.mock.Expectation
import org.mockserver.model.*
import org.mockserver.model.JsonBody.json
import utils.getIsoCurrentDate
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue

const val TTL_SEC: Long = 600

class UserHandleExpectation : ExpectationResponseCallback {
override fun handle(httpRequest: HttpRequest): HttpResponse {
val gson = Gson()
val mockServerClient = MockClient()

// Convert POST payload to data structure
val userPayload: UserPOSTRequestDTO? =
gson.fromJson(httpRequest.bodyAsJsonOrXmlString, UserPOSTRequestDTO::class.java)

// Create expectation for GET /users/{userId}
val userUUID = UUID.randomUUID().toString()
val userGETResponse = UserGETResponseDTO(
userUid = userUUID,
name = userPayload?.name,
surname = userPayload?.surname,
currency = userPayload?.currency,
region = userPayload?.region,
serverCode = userPayload?.server_code,
createdDate = getIsoCurrentDate()
)
val userExpectation = Expectation.`when`(
HttpRequest.request()
.withMethod("GET")
.withPath("/api/v1/users/${userUUID}"),
Times.unlimited(), TimeToLive.exactly(TimeUnit.SECONDS, TTL_SEC)
).thenRespond(
HttpResponse.response()
.withContentType(MediaType.APPLICATION_JSON)
.withBody(json(gson.toJson(userGETResponse, UserGETResponseDTO::class.java)))
)

// Create expectation for GET /users/{userId}/accounts/{accountId}
val accountId = userUUID.hashCode().absoluteValue
val accountGETResponse = AccountGETResponseDTO(
id = accountId,
userUid = userUUID,
currency = userPayload?.currency,
status = "ACTIVE",
expired = false
)
val userAccountsExpectation = Expectation.`when`(
HttpRequest.request()
.withMethod("GET")
.withPath("/api/v1/users/${userUUID}/accounts/${accountId}"),
Times.unlimited(), TimeToLive.exactly(TimeUnit.SECONDS, TTL_SEC)
).thenRespond(
HttpResponse.response()
.withContentType(MediaType.APPLICATION_JSON)
.withBody(json(gson.toJson(accountGETResponse, AccountGETResponseDTO::class.java)))
)

// Prepare response for current callback
val userPOSTCallbackResponse = UserPOSTResponseDTO(
userId = userUUID,
name = userPayload?.name,
surname = userPayload?.surname,
account = accountId,
serverCode = userPayload?.server_code,
region = userPayload?.region
)

// Store expectations in Mockserver by one request
mockServerClient.setExpectations(
listOf<Expectation>(
userExpectation,
userAccountsExpectation
)
)
return HttpResponse.response()
.withStatusCode(HttpStatusCode.CREATED_201.code())
.withContentType(MediaType.APPLICATION_JSON)
.withBody(json(gson.toJson(userPOSTCallbackResponse, UserPOSTResponseDTO::class.java)))
}
}

Following all of these steps will lead you to an easy-to-use tool that generates the required responses to your subsequent calls. In addition, it ensures a more flexible precondition creation in the test framework. Now you can directly change the mock-prepared expectation in the selected tests. For example, the mock has prepared a series of responses, but you want to get a special parameter value at a certain test step. In this case, you can request this expectation from MockServer, change the parameter, and update the expectation, providing the test with the data you want. In the end, it saves you from having to write the procedure for creating typical preconditions in each test; the mock does it instead.

Conclusion

Using MockServer capabilities, we have implemented an easy-to-use and versatile request-mocking tool. It allows us to flexibly manage data during tests and to isolate the third-party service logic. You can find all MockServer capabilities, request matching methods, verification, and more in this detailed documentation. The code used in the examples can be found here.

Hope this saves you some time and headache, and have fun mocking!

--

--