I don't need to enumerate all the situations in which you would need to mock an API or an HTTP server. There are many options available, such as faker. This article delves into MockServer and shows how to set up a mock server using only Docker and a JSON file. Let's get started!
🤯 🔥 MockServer can also be used as a proxy (to record and potentially modify requests/responses), or a combination of mock and proxy!
The docker-compose
First, let's create a docker-compose for running the latest version of MockServer:
services:
mockserver:
image: mockserver/mockserver:latest
ports:
- "1080:1080"
environment:
MOCKSERVER_PROPERTY_FILE: /config/mockserver.properties
MOCKSERVER_INITIALIZATION_JSON_PATH: /config/spec.json
# ↓ hot reload when the spec.json changes 😍
MOCKSERVER_WATCH_INITIALIZATION_JSON: "true"
volumes:
# bind the config for easy editing
- ./config:/config
Nothing special here. We are using a volume for the config and telling MockServer to load the endpoints to mock from the ./config/spec.json
file.
Before starting it, let's create the necessary files (we will see how to use them later):
mkdir config
touch config/mockserver.properties
echo '[]' > config/spec.json
Start the mock server by executing:
docker compose up -d
MockServer is now running on http://localhost:1080! However, since the spec.json
- defining the expectations, or endpoints to mock - is empty, it only returns 404's. Let's change that!
💡 the mockserver.properties
will stay empty for this article, but I recommend you check out the configuration documentation for all the possibilities it offers! Dynamic SSL certificates, logging, metrics, etc.
Defining the expectations (spec.json)
🔥 More examples available at https://github.com/mock-server/mockserver/blob/master/mockserver-examples/json_examples.md 🔥
The most basic expectation
The spec.json
file defines expectations, aka the endpoints to mock and what to return. It contains an object or an array of objects defining the HTTP request to match, and the response to return.
Unclear? Here is a basic example:
{
"httpRequest": {
"path": "/hello"
},
"httpResponse": {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": "{\"hello\": \"world\"}"
}
}
Save the spec.json
, wait a second and try hitting http://localhost:1080/hello. You should see the body {"hello": "world"}
.
This is just a simple example, but we can go way deeper. The request may define headers, method, path and query parameters, authentication, and more, all using either string or regex matching. MockServer even supports stuff such as adding delay in the response or behaving differently depending on how often the endpoint is called!
Using templates
The response often depends on the request, and enumerating all the possible input/output pairs is tedious. MockServer supports loops, conditionals, and 3 response template engines: Javascript, mustache, and velocity.
Let's see a velocity example (at the time of writing, Javascript is broken in Docker, see the related issue):
{
"httpRequest": {
"path": "/cart/item/{id}/",
"pathParameters": { "id": ["\\d+"] }
},
"httpResponseTemplate": {
"templateType": "VELOCITY",
"template": "#set($id = $!request.pathParameters['id'][0]) #if ($id == 1) {\"statusCode\": 500} #else {\"statusCode\": 200, \"body\": \"{\\\"name\\\": \\\"foo\\\"}\"} #end",
}
}
What is happening? Instead of specifying the httpResponse
directly, we ask MockServer to "compute" it by interpreting a template. In the example above, the server returns a 500
for the item 1
and a JSON body otherwise.
Things to note:
#set
,#if
/#end
,#foreach
/#end
, etc. are MockServer constructs independent of the template type, allowing for complex logic inside the response.$id
is a MockServer local variable, but$!request
(notice the!
) is a velocity template directive.there is full support for regular expressions in all request attributes.
Request paths and query parameters can be named and accessed in the response. The index
[0]
is necessary because MockServer uses Java multivalued maps under the hood. Here, using the request path/cart/item/\\d+/
and the condition($request.path == '/cart/item/1/')
would have been simpler.Since the template itself is a JSON string, multiple escapes of
"
are necessary and no wrap is possible, making it difficult to read... The joys of JSON 😆.
Let's check the result of the above template:
➜ curl --fail-with-body http://localhost:1080/cart/item/13/
{"name": "foo"}
➜ curl --fail-with-body http://localhost:1080/cart/item/1/
curl: (22) The requested URL returned error: 500
Request attributes (headers, body, path, remote address, etc.) aside, other magic variables are at our disposal in templates. For example:
$!now_epoch
→ get the current timestamp seconds since January 1, 1970 (UNIX epoch)$!uuid
→ get a random UUID$!rand_int_100
→ get a random number between 0 and 99...
Check the docs for more!
Importing the request spec from OpenAPI
Often, you want to simulate existing endpoints from a known API. If the latter has an OpenAPI schema, MockServer can parse it to construct the request matchers.
Let's use the dev.to API as an example, and simulate the GET /api/articles
endpoint: https://developers.forem.com/api/v1#tag/articles/operation/getArticles.
{
"httpRequest": {
"specUrlOrPayload": "https://developers.forem.com/redocusaurus/plugin-redoc-1.yaml",
"operationId": "getArticles"
},
"httpResponse": {
"statusCode": 200,
"body": "[{\"name\": \"super article\"}]"
}
}
The two lines of the httpRequest
above transform into a 93-line long request matcher. Here is an abridged version:
{
"method": "GET",
"path": "/api/articles",
"headers": { "api-key": [".+"] },
"queryStringParameters":
{
"keyMatchStyle": "MATCHING_KEY",
"?username": {"parameterStyle": "FORM_EXPLODED", "values": [{"schema": {"type": "string"}}]},
"?top": { "parameterStyle": "FORM_EXPLODED", "values": [{"schema": {"format": "int32", "minimum": 1, "type": "integer"}}]},
// ...
}
}
When importing from OpenAPI, the request matchers are way more precise. For instance, the dev.to schema requires an authentication header. Without it in a request, there is no match, and the MockServer returns a 404. It doesn't complain about invalid query parameters though, which is expected.
# Missing the authorization header
➜ curl --fail-with-body http://localhost:1080/api/articles
curl: (22) The requested URL returned error: 404
# ok
➜ curl --fail-with-body -H 'api-key: x' http://localhost:1080/api/articles
[{"name": "super article"}]
Going further
To see more, check out their documentation and the examples. Even better, try it out yourself!
Conclusion
MockServer is a lightweight, convenient solution for mocking APIs. The Docker image is only 276MB! A basic setup only requires Docker and a JSON file, which is powerful enough for more use cases. The hot reloading feature makes it easy to set up and test.
However, while a JSON file has its advantages, especially when paired with Docker, it can be challenging in terms of readability and comprehension. For complex APIs, opting to define specifications in Java or Javascript and then exporting them to JSON might be a better alternative. To learn more, check out Creating expectations.
This article only covered the basics, but I hope it got you interested in trying out MockServer the next time you need to fake an HTTP(s) endpoint.
Thanks for reading, and happy coding!