When it comes to mocking services, either for development or testing purposes, there are many options available and it’s not always an easy task. Mocking helps improve productivity by providing the ability to quickly test and develop against endpoints with varying conditions (e.g. a service being unavailable), as well as being able to provide quicker feedback when compared to trying to test and/or development against real services. There are basic options, like mocking at a function level for unit testing and then there are off-the-shelf products like CA Lisa (which require additional training to use and can be expensive) for mocking whole systems.

In this blog I’m going to cover mocking using mountebank – an open source, lightweight tool for stubbing and mocking endpoints. I’ll go over a couple of simple examples on how to setup and use mountebank for testing – with a hard coded configuration and another method that involves setting up a new configuration for each test.

Having come across a project where the requirement was to implement and test several hundred microservices together, it prompted me to explore mocking servers in more detail – in the hope of finding something that would aid the team in the development of these microservices.  I felt we needed something more substantial than a simple mocking solution, something which would allow us to fully test the large number of microservices but at the same time allow us to have control over the responses coming back. I started off trying out a Java based mocking server, but quickly realised that it was difficult to do things like injections and state changes. On top of this, I also wanted to find something within the JavaScript ecosystem, as this is what the team had more experience in and was more comfortable with. Then I came across mountebank – with its rich feature set, flexibility, simplicity of use and having a range of options to handle responses – it made it a good choice for us to use as our mocking server.

Overview

What is Mountebank?

Mountebank is an open source, lightweight tool for stubbing and mocking HTTP, HTTPS, SMTP and TCP services, that any application under test can use, instead of the real service. The mock service can be configured to return predefined responses or proxy to the real service. By simply pointing the application under test to use the mountebank server instead of real dependencies, you can test the application under test like you would if it were connected to the real services.

Benefits of using mountebank:

  • Easy to install – mountebank is easy to get started.
  • Platform not just a tool – mountebank aims to be fully cross-platform, with native language bindings. Mountebank allows JavaScript injection for predicates and response types, for situations when the built-in ones are not sufficient.
  • Powerful – mountebank is an open source service virtualisation tool that can handle multiple protocols and comes without any platform constraints. 

Mountebank in action

The following is an example of how to set up mountebank so you can see it in action:

Mountebank centres around imposters. An imposter defines how a mock service should work. They can be set via a configuration file or via HTTP endpoints that is available once mountebank is started. An imposter contains one or more stubs that define how to handle requests to the service mock.

A stub returns a set response based on the request. A stub can contain a list of responses and a list of predicates (that defines the criteria that needs to be met for a particular response to be returned).

Pre-requisites

Install

Mountebank can be installed using npm

npm install -g mountebank

Setting it up once using .ejs file

Here, the imposter is defined in an embedded javascript (.ejs) file, which is just a client side templating language. An imposter defines things like port, protocol, name and stubs (there can be one or more).

Folder Structure:
mountebank/
├── imposters/
│   ├── sampleImposter.ejs
├── stubs/
│   ├── sampleStub.json
├── imposters.ejs
└── package.json

There can be one or more imposters depending on what services you need to mock to sufficiently test the application. For example, you may have 3 services that the application interfaces with, so you may choose to have 3 imposter files to represent each service.

imposters.ejs

The imposters.ejs contains the list of imposter files for mountebank to look at.

{
"imposters": [
    <% include imposters/sampleImposter.ejs %>
]
}

Let’s define one stub in sampleImposter.ejs, call it sampleStub.json. A stub will define how to respond to incoming requests.

The following imposter tells mountebank to respond to http requests on port 4545.

{
"port": 4545,
"protocol": "http",
"name": "sample stub",
"recordRequests": true,
"stubs": [
    <% include ../stubs/sampleStub.json %>
]
}

Note: if stubs section is left empty e.g “stubs”: [] , requests will always return a 501 response, which is the default response.

A stub uses predicates to define the rules for that request must to match. Responses are returned only if all the predicates in the stub matches. In the following sampleStub.json, the stub will look for a POST request and a specified path matching “/test”. It will then return a 200 response – if a match is found, if not, it will look for the next stub defined in the imposter or use the default response – if no response can be matched.

sampleStub.json

{
"predicates": [
  {
    "equals": {
      "method": "POST",
      "path": "/test"
    }
  }
],
"responses": [
  {
    "is": {
      "statusCode": 200
    },
"body": {}
}
] }

We can further improve the stub by setting the body of the response to reference a file, letting mountebank stringify it for us.

"body": "<%- stringify(filename, '<filePath>') %>"

By adding a script (mock) to the package.json, a shortcut to execute mountebank using the specified config file imposters.ejs has been setup.

package.json
{
  "name": "test",
  "description": "test service using mocha and supertest.",
  "version": "0.0.1",
  "license": "UNLICENSED",
  "scripts": {
      "mock": "mb --configfile mountebank/imposters.ejs --loglevel debug"
  },
  "dependencies": {
      "mountebank": "^1.14.0"
  }
}

We can now start up mountebank with the command:

npm run mock

By default, mountebank will run on port 2525. Once started, you will be able to view your mountebank setup on http://localhost:2525/. Also, if you are running up your services locally, then you will need to kill any services that will be running on the same port as the mountebank stub. For example, our stub will be mocking the service on port 4545, if I have the service running locally on port 4545, this will conflict with the mountebank setup and mountebank will not be able to start up a mock on a port that is already in use.

So now that you have a mountebank mock running, when you start up you application, and when you hit the service the response you get back will be served from the mountebank mock instead of the real service.

Result of npm run mock:

➜  test-with-mountebank:  
> mb --configfile mountebank/imposters.ejs --loglevel debug
info: [mb:2525] mountebank v1.14.1 now taking orders - point your browser to http://localhost:2525 for help
debug: [mb:2525] config: {"options":{"help":false,"configfile":"mountebank/imposters.ejs","loglevel":"debug","port":2525,"noParse":false,"pidfile":"mb.pid","nologfile":false,"logfile":"mb.log","allowInjection":false,"localOnly":false,"ipWhitelist":["*"],"mock":false,"debug":false,"heroku":false},"process":{"nodeVersion":"v10.0.0","architecture":"x64","platform":"darwin"}}
info: [mb:2525] PUT /imposters
debug: [mb:2525] ::ffff:127.0.0.1:58772 => {"imposters":[{"port":4545,"protocol":"http","name":"sample stub","recordRequests":true,"stubs":[{"predicates":[{"equals":{"method":"POST","path":"/gateway/ExternalGetNniNamePortV3"}}],"responses":[{"is":{"statusCode":200}}]}]}]}
info: [http:4545 sample stub] Open for business...

The important thing to note is the following message: info: [http:4545 sample stub] Open for business...  if you don’t get this message, that means mountebank has not started up correctly. Just stop mountebank (Ctrl + C) and run again using npm run mock

Mountebank is now ready for use.

Setting it up dynamically using .js files

When using mountebank, it is possible to dynamically add/remove imposters to suit the needs of your test. For example, you may need to hit a valid service in the beginning of the test, but only after reaching a certain point do you want the service to fail – to verify a particular fail test scenario. In this instance, being able to dynamically change when the service is expected to fail becomes useful.

In the scenario where we want to hit a downstream service (which doesn’t yet exist) and so we are going to mock the service using mountebank, so we can continue to development and testing. To do so, we have create a sample response (sampleResponse.xml) and load it into mountebank. So when we hit the service, we can get a response and be able verify our service under test.

The following example uses Jest test framework with SuperTest. This is a simple example of how to mock a service with mountebank. For simplicity, our service will return an XML file, but any file format can be returned.

It is also worth noting that the imposter config is being loaded as part of the test setup and not defined prior (that is, it is not defined as part of the mountebank config).

Folder Structure

mountebank/
├── mountebank-helper.js
├── package.json
├── properties.js
├── stubs
│   └── sampleStubConfig.js
└── tests
   ├── sampleResponse.xml
   └── sampleTest.spec.js

sampleTest.spec.js

const props = require('../properties')
const request = require('supertest')
const mountebankHelper = require('../mountebank-helper')
const readFile = require('read-file-utf8')
const flattenXml = require('flattenxml')
describe('operation: getAddress', () => {
beforeAll(() => {
  mountebankHelper.setUrl(props.mountebankUrl)
})
describe('valid Scenarios', () => {
  test('Scenario: post mountebank stub dynamically', () => {
    const responseBody = readFile('./tests/sampleResponse.xml')
    const sampleImposterConfig = require('../stubs/sampleStubConfig')([{ statusCode: 200, responseBody: responseBody }])
    return mountebankHelper
      .createImposter(sampleImposterConfig)
  })
})
})

sampleResponse.xml

<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>

package.json

{
"name": "test",
"description": "test service using mocha and supertest.",
"version": "0.0.1",
"license": "UNLICENSED",
"scripts": {
  "test": "jest",
  "format": "prettier-standard 'tests/**/*.js', '**/*.js'",
  "mock": "mb --loglevel debug --allowInjection"
},
"dependencies": {
    "envobj": "1.0.5",
    "read-file-utf8": "1.1.2",
    "flattenxml": "^1.0.0",
    "pino": "4.16.1",
    "mountebank": "^1.14.0",
    "supertest": "3.0.0",
    "jest": "22.4.3"
}
}

Note: the dependencies listed in package.json ( e.g. ‘read-file-utf8’, ‘flatmap’, ‘supertest’ and ‘jest’) are all just examples for a test setup. This is only to provide a guide and can vary depending on your own project needs.

Having a separate properties.js file to define setup properties (that will be used across the project) is a good practice. As the setup properties are defined in a single location, and once used can be updated from a single point as well.

properties.js

const envobj = require('envobj')
const pino = require('pino')()
const env = envobj({
DEBUG: false,
// Mountebank mocking server
MB_PROTOCOL: 'http',
MB_HOST: 'localhost',
MB_PORT: 2525,
// Mocked services
MB_PORT_FOR_SAMPLE_STUB: '4545',
SAMPLE_SERVICE_ADDRESS: '/sampleServicesPath'
})
if (env.NODE_ENV !== 'production') {
pino.info('-- ENVIRONMENT CONFIG --')
Object.keys(env).map(key => {
  pino.info(`${key}: ${env[key]}`)
})
}
const mountebankUrl = `${env.MB_PROTOCOL}://${env.MB_HOST}:${env.MB_PORT}`
// Mocked services
const mbPortSample = env.MB_PORT_FOR_SAMPLE_STUB
const sampleServiceAddress = env.SAMPLE_SERVICE_ADDRESS
module.exports = {
mountebankUrl,
mbPortSample,
sampleServiceAddress
}

sampleStubConfig.js is just a collection of predicates and responses. As mentioned earlier in the blog, responses are returned only if the stub matches in predicates. We can set up the virtual service with a specific responseA response body generally is a string or an object.

sampleStubConfig.js

const props = require('../properties')
module.exports = function (statusCode, responseBody) {
return {
  port: props.mbPortSample,
  protocol: 'http',
  name: 'Sample stubs',
  recordRequests: true,
  stubs: [
    {
      predicates: [
        {
          equals: {
            method: 'POST',
            path: props.sampleServiceAddress
          }
        }
      ],
      responses: [
        {
          is: {
            statusCode,
            body: responseBody
          }
        }
      ]
    }
  ]
}
}

mountebank-helper.js

const request = require('supertest')
let mountebankServer = null
function setUrl (url) {
mountebankServer = request(url)
}
function createImposter (imposterConfig) {
return mountebankServer
  .delete(`/imposters/${imposterConfig.port}`)
  .then(() => {
    return mountebankServer
      .post('/imposters')
      .set('Content-Type', 'application/json')
      .send(JSON.stringify(imposterConfig))
  })
}
function getImposterPort (imposterConfig) {
return imposterConfig.port
}
function getRequests (port) {
return mountebankServer.get(`/imposters/${port}`).then(response => {
  return {
    numberOfRequests: response.body.numberOfRequests,
    requests: response.body.requests
  }
})
}
module.exports = {
setUrl,
createImposter,
getImposterPort,
getRequests
}

createImposter() deletes the existing stubs (if any)  from the mountebank server and then loads it with sampleStubConfig.js. 

Start up mountebank

npm run mock

By default, mountebank will run on port 2525. Once started, you will be able to view your mountebank setup on http://localhost:2525/

Result of npm run mock:

➜  test-with-mountebank: npm run mock
> mb --loglevel debug --allowInjection
info: [mb:2525] mountebank v1.14.1 now taking orders - point your browser to http://localhost:2525 for help
debug: [mb:2525] config: {"options":{"help":false,"loglevel":"debug","allowInjection":true,"port":2525,"noParse":false,"pidfile":"mb.pid","nologfile":false,"logfile":"mb.log","localOnly":false,"ipWhitelist":["*"],"mock":false,"debug":false,"heroku":false},"process":{"nodeVersion":"v10.0.0","architecture":"x64","platform":"darwin"}}
warn: [mb:2525] Running with --allowInjection set. See http://localhost:2525/docs/security for security info

So now that you have a mountebank mock running, when you run jest test, it will load response to mountebank.

Run following command in new terminal window.

npm run test

Check mountebank terminal window to see the log, you should see contents of sampleResponse.xml displayed in the log as a match for the response is found .

debug: [mb:2525] ::ffff:127.0.0.1:55458 => {"port":"4545","protocol":"http","name":"Sample stubs","recordRequests":true,"stubs":[{"predicates":[{"equals":{"method":"POST","path":"/sampleServicesPath"}}],"responses":[{"is":{"statusCode":[{"statusCode":200,"responseBody":"<note>\n  <to>Tove</to>\n <from>Jani</from>\n <heading>Reminder</heading>\n <body>Don't forget me this weekend!</body>\n</note>"}]}}]}]}
info: [http:4545 Sample stubs] Open for business...

So now we’ve gone through a couple of simple examples of how to use mountebank, using a static imposter.ejs file and another using dynamic .ejs files. Hope this has given you a taste of how simple it is to setup and use mountebank.

Happy Mocking 😀 !!!