Alert Message
Thanks for signing up! We hope you enjoy our newsletter, The Teller.
Mountebank
engineering

Developing and Testing with Mountebank

By Peter Prakobkit

We’ve written about our monolith, and our journey migrating to Kubernetes as we move towards having more microservices. This change has also caused us to upgrade our toolchain, like how we run Jenkins. In this piece, I’d like to introduce you to a tool that we’ve been using a lot lately. A tool that enables us to achieve close to feature parity to our staging and production environments at a relatively low cost. This tool is also great for testing, which I’ll also talk about here. This tool is Mountebank.

Mountebank is an open source over the wire test double. It allows us to stub external libraries, like an email provider that we interact with, or other Earnest microservices that a service depends on. At a high level, it allows us to quickly (and cheaply) spin up the dependencies for a service under development. Before we go into some examples of how we use Mountebank, I will first go through a quick overview of the core concepts and entities we work with.

Imposters

The main entity of Mountebank is an imposter. Imposters are test doubles that can be reached via a protocol and a port. An imposter is basically a server, or dependency, that we want to stub. For example, if our system has a dependency on an HTTP server called foo, we can assign an imposter in Mountebank to stub the foo service. You can also declare multiple imposters running in parallel!

Stubs, Predicates, and Responses

For each imposter, you can define a list of stubs. Each stub supports having a list of predicates and a list of responses. The predicate is a condition that determines whether a given stub is responsible for responding. When the predicate is true, the response associated with that predicate will be returned.

In our previous example, if system foo has a couple of endpoints, namely, GET /bar and POST /quz/:id, where idhas to be a uuid, for example, we can define stubs for each of the endpoints. You would define a predicate that matches on a GET method and /bar path for the first endpoint. This predicate would have a canned response for what it should return. Similarly, you would have another stub that has a predicate that matches on the method POST and quz/:id path. Mountebank supports regex so you could write a regex that matches on UUIDs. Responses could be canned responses like an XML or JSON object, or it can be dynamically generated using JavaScript (via mountebank’s injection feature), which makes this tool very powerful.

With these concepts in mind, I will now provide two examples of how we use Mountebank at Earnest. The first is using it to stub external dependencies for local development, and second, is how we use it in our test.

Stubbing External Dependencies

We love docker and use it quite a bit for our local development. We setup mountebank as a service in our docker-compose project, where we mount the imposter and setup files into the container. Here is an example:

FROM node8-image
WORKDIR /usr/src/app
ADD . /usr/src/app
RUN npm install -g mountebank
RUN npm install -g node-fetch
CMD ["mb", "start", "--mock", "--configfile", "imposters.ejs", "--allowInjection"]
EXPOSE 3000
view raw Dockerfile hosted with ❤ by GitHub

We set up our imposter by dependencies and inject the files into the root config for mountebank; say a project has 2 dependencies, we would have 2 imposter files. Each imposter has its own predicates and stubs, and each runs on a different port. For the following example, the foo service can be reachable on the host that mountebank is running on and on port 3013, i.e. http://mountebank:3013. Note that we start mountebank with the --allowInjection flag which enables us to write JavaScript to dynamically generate the payload:

{
"imposters": [
<% include foo.ejs %>,
<% include bar.ejs %>
]
}
view raw imposters.ejs hosted with ❤ by GitHub
{
"port": 3013,
"protocol": "http",
"name": "foo",
"defaultResponse": {
"statusCode": 404
},
"stubs": [
{
"responses": [
{
"inject": "<%- stringify(filename, 'foo.js') %>"
}
],
"predicates": [
{
"contains": {
"method": "GET",
"path": "/foo"
}
}
]
}
]
}
view raw foo.ejs hosted with ❤ by GitHub
function fooResponse(request, state) {
return {
statusCode: 200,
body: [
{ "foo": "bar" }
]
};
}
view raw foo.js hosted with ❤ by GitHub

Using mountebank to stub external dependencies allows us to closely mimic a staging or production environment, especially when having a staging or local environment for a third party dependency is not possible.

Mocking Verification

Another example of how we use mountebank is in how we test through mock verifications. In this style, we test that the right calls were made (as opposed to testing that some state change has occurred). We’ve observed that these types of test are great for testing external dependencies, like an email provider with a concrete API, where we assert on the calls made; testing state changes in an external dependency is sometimes difficult and expensive, or even impossible. Lastly, this allows us to test our systems from the outermost boundary.

Compared to the previous example where we declare our imposters on start-up, we can create and remove imposters during by making requests to the mountebank API, which by default runs on port 2525. This is perfect for testing as you can create imposters during the setup phase, and remove it during teardown.

In this example, we want to test that our calls to an email provider conforms to the API. During setup we create an imposter:

import request from "request-promise";
const setup = async () => {
// delete the current imposter running on http://mountebank:3014
await request({
method: "DELETE",
uri: "http://mountebank:2525/imposters/3014"
});
// create a new imposter on http://mountebank:3014
await request({
method: "POST",
json: true,
uri: "http://mountebank:2525/imposters",
body: {
port: "3014",
protocol: "http",
name: "origin",
defaultResponse: {
statusCode: 404,
body: "Error"
},
stubs: [
{
predicates: [
{
contains: {
method: "POST",
path: "/emails"
}
}
],
responses: [
{
is: {
statusCode: 201,
body: {
status: "success"
}
}
}
]
}
]
}
});
};
view raw setup.js hosted with ❤ by GitHub

Then during the test, we would make a call in application code to the email provider. We usually pass the URL as a configuration so we would pass the URL that points to mountebank as part of setting up the test dependencies:

import request from "request-promise";
const sendEmail = async () => {
await request({
method: "POST",
json: true,
uri: "http://mountebank:3014/emails", // this is hardcoded but it could be parameterized by environment
body: {
to: "[email protected]",
from: "[email protected]",
content: "Hi!"
}
});
}
view raw sendEmail.js hosted with ❤ by GitHub

Lastly, we can call mountebank to verify the behavior:

import request from "request-promise";
const assertEmailSent = async () => {
const response = await request("http://mountebank:2525/imposters/3014");
const emailsSent = JSON.parse(response).requests.filter(r => r.path === "/emails" && r.method === "POST");
assert.equal(emailsSent.body.to, "[email protected]");
}
view raw test.js hosted with ❤ by GitHub

The examples shown here are only a few features of mountebank that we use. There are many more, such as defining behaviors like waiting, or proxies to record and replay responses from an actual dependency.

At Earnest, we are constantly experimenting with and learning new tools to improve our testing practices so we can write better software and sleep better at night. If you would like to learn more, Mountebank has great documentation, or want to join our team, Earnest is hiring!

Disclaimer: This blog post provides personal finance educational information, and it is not intended to provide legal, financial, or tax advice.