The Problem
In today’s micro service world it is common for a service to rely on many others. To effectively test a service we often need more than just unit tests but also need to test the interactions with dependencies. Unfortunately that can be difficult. It might be easy to standup those service dependencies in a test environment. Or it might be hard to populate with realistic tests data. Or they might be unreliable which could result it flaky tests.
Solution
To get around the above problems we can mock our service dependencies. My team explored a number solutions but arrived at using a tool called Mountebank due to it’s level of activity, feature set, simplicity, and our team’s existing comfort with the JavaScript ecosystem.
Mountebank allows our team to create mock services that our app can use instead of the real service. The mock service can be configured to return a predefined response or proxy to the real service and record the response.
Imposters, Stubs, and Predicates
Mountebank centers around Imposters. An Imposter defines how a mock service should work. They are defined in JSON and can be set via a configuration file or through a rest API that is available once Mountebank is started. An Imposter contains one or more Stubs that define how to handle requests to the service mock.
The following Imposter instructs to Mountebank to respond to http requests on port 4547. So far we haven’t defined any Stubs so requests will always get a 501 response (notice the defaultResponse
).
// I'm an imposter and I will fool your app
{
"protocol": "http",
"port": 4547,
"name": "service-foo",
"defaultResponse": {
"statusCode": 501
},
"stubs": []
}
Next up in the configuration is the Stub. The Stub defines how to respond to incoming requests. A Stub uses Predicates to define rules that requests must match. If all the Predicates in a stub match then the responses are returned. Notice responses
is an array. If multiple responses are defined in a Stub it’ll cycle through them for each request.
The following Stub it is looking for a GET request and a path that matches the regular expression. If they don’t match it’ll go to the next Stub defined in the Imposter or the defaultResponse
if none match.
{
"predicates": [
{
"equals": {
"method": "GET"
}
},
{
"matches": {
"path": "^/users/.*/permissions"
}
}
],
"responses": [
{
"is": {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{ \"foo\": \"bar\" }"
}
}
]
}
The above Stub would match GET /users/1234567/permissions
and return a response with a 200 status code and a simple JSON object. JSON bodies in Stubs must be stringified. We can improve the Stub by referencing a JSON file and letting Mountebank stringify it for us.
"body": "<%- stringify(filename, '../fixtures/service-foo/user-permissions-200.json') %>"
Now Mountebank will return the JSON defined in the file.
Mountebank has many Predicate types. You can match on nearly any part of a request. For example, in our Hydra Imposter we have a Predicate that looks for a particular key to exist in the request body.
{
"jsonpath": { "selector": "$..field" },
"equals": { "body": "id" }
}
Through these Predicates you can build up complex queries to match nearly any kind of request.
Potential Problems
While mocking service dependencies aids in testing there are a number of risks.
Out of sync mocks
As with all mocking there is a risk of becoming out of sync with the actual implementation. Our hope is to mitigate this in a number of ways.
- Rely on contract testing where possible to validate our mocks. This would allow us to validate each part of our app in isolation without relying on brittle end to end tests as much. There are tools such Pact that can enable this.
- When contract tests are not a viable options run API and UI tests against staging and production (Post Deployment Verification).
Complex Imposters
There are many ways to configure Imposters and if not careful they can become extremely complex. Here are a few observations I’ve made as I’ve used Mountebank:
- Focus mostly on mocking happy paths and common problem areas. It is possible to mock out every edge case but that could mean the Imposters likely getting bloated and confusing.
- Simplify Predicates as much as possible. Use only those that are absolutely required.
- Break up Imposters, Stubs and responses into separate files. We’ve broken the configuration into a file per Imposter (service) and then each Stub includes the necessary JSON files. It keeps the config organized and easier to follow and add to.
- Decide on common identifiers that can be used to easily simulate a response. For example, an ID of 11111 (
GET /users/11111/permissions
) could be used to always return a 200 response but an ID of 22222 could always return a 404. An ID of 33333 could result in a 500 error code.
Future
Our next step is to continue to build out more tests that rely data provided by Mountebank. Additionally we are investigating Post Deployment Verification and Contract testing as noted above. As usual the higher you get in the testing pyramid the more difficult and brittle tests get so we’re still figuring what to test and how much to test.
I’m always curious to see how others do testing so please get in touch and we can chat!
Additional Documentation