Come build the future of education financing.
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.
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
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 set up Mountebank as a service in our docker-compose project, where we mount the imposter and setup files into the container. Here is an example:
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
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.
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 tests 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 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:
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:
Lastly, we can call Mountebank to verify the behavior:
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!