mountebank

mountebank - over the wire test doubles

Fork me on GitHub

the apothecary

Injection

mountebank allows JavaScript injection for predicates and response types for situations where the built-in ones are not sufficient

Injection only works if mb is run with the --allowInjection flag.

Though mountebank has gone to some trouble to make injections as hassle-free as possible, there are a couple of things to be aware of. First, when validating stub creation, mountebank does not validate any injected functions. Second, all injections have full access to a node.js runtime environment, including the ability to require in different modules. Of course, such power comes with its own challenges, which leads us to third, with injections, you can crash mountebank. Though mountebank has gone to some effort to be robust in the face of errors, he is not as clever as you are. If you find yourself coding injections frequently because of missing functionality that you believe would be generally useful, mountebank humbly requests you to add a feature request.

There are four options for injecting JavaScript into mountebank. The first two, predicate injection and response injection, are described below. The third, post-processing responses through the decorate behavior, is described on the behaviors page. Finally, determining the end of a TCP request is described on the tcp protocol page.

Predicate injection

Predicate injection allows you to pass a JavaScript function to decide whether the stub should match or not. Unlike other predicates, predicate injections bind to the entire request object, which allows you to base the result on multiple fields. They can return truthy or falsy to decide whether the predicate matches. mountebank doesn't know what that means, so he always returns true or false.

The injected function should take a parameter representing the field it is bound to, and, optionally, a logger. Predicate injection must be strictly synchronous.

The following example uses injection to satisfy a complicated multi-field predicate for HTTP:


POST /imposters HTTP/1.1
Host: localhost:9497
Accept: application/json
Content-Type: application/json

{
  "port": 4545,
  "protocol": "http",
  "stubs": [
    {
      "responses": [{ "is": { "statusCode": 400 } }],
      "predicates": [{
        "inject": "function (request, logger) {\n\n    function hasXMLProlog () {\n        return request.body.indexOf('<?xml') === 0;\n    }\n\n    if (request.headers['Content-Type'] === 'application/xml') {\n        return !hasXMLProlog();\n    }\n    else {\n        return hasXMLProlog();\n    }\n}"
      }]
    }
  ]
}

Injections certainly don't help the readability of the JSON. If you're loading imposters through a config file, look at the stringify function supported in file templating with the --configfile command line option to support storing the functions in a readable format. Let's look at our injected function appropriately formatted:


function (request, logger) {

    function hasXMLProlog () {
        return request.body.indexOf('<?xml') === 0;
    }

    if (request.headers['Content-Type'] === 'application/xml') {
        return !hasXMLProlog();
    }
    else {
        return hasXMLProlog();
    }
}

It matches on XML content missing a prolog, or a prolog added to non-XML content. First we'll send a request that matches the first condition:


POST /test HTTP/1.1
Host: localhost:4545
Content-Type: application/xml

<customer>
  <name>Test</name>
</customer>

HTTP/1.1 400 Bad Request
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

Now we'll match on the second condition:


POST /test HTTP/1.1
Host: localhost:4545
Content-Type: application/json

<?xml>
<customer>
  <name>Test</name>
</customer>

HTTP/1.1 400 Bad Request
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

Response injection

Response injection allows you to dynamically construct the response based on the request and previous requests. The response object should match the structure defined in the appropriate protocol page linked to on the sidebar. mountebank will use the default value documented on the protocol page for any response fields you leave empty.

mountebank maintains a state variable for you that will be empty on the first injection. The same variable will be passed into every stub inject function, allowing you to remember state between calls, even between stubs.

Asynchronous injection is supported through a callback parameter. If your function returns a value, the imposter will consider it to be synchronous. If it does not return a value, it must call the callback parameter with the response object. The following injection takes advantage of the node.js environment it will run in to proxy most of the request to localhost:5555. It records the request and response to the state variable, and only proxies if certain request parameters have not already been sent.

The example below will use two imposters, an origin server and a server that proxies to the the origin server. It would be better implemented using a proxy response type, but has sufficient complexity to demonstrate many nuances of injection.

First we'll create the origin server at port 5555. To help us keep track of the two imposters, we'll set the name parameter, which will show up in the logs.


POST /imposters HTTP/1.1
Host: localhost:9497
Accept: application/json
Content-Type: application/json

{
  "port": 5555,
  "protocol": "http",
  "name": "origin",
  "stubs": [
    {
      "responses": [{ "inject": "function (request, state, logger) {\n    logger.info('origin called');\n    state.requests = state.requests || 0;\n    state.requests += 1;\n    return {\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({ count: state.requests })\n    };\n}" }]
    }
  ]
}

The injected function for the origin server imposter is formatted below. This uses the state parameter, and sets the requests field on it to return how many times it's been called:


function (request, state, logger) {
    logger.info('origin called');
    state.requests = state.requests || 0;
    state.requests += 1;
    return {
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ count: state.requests })
    };
}

Now let's create the proxy imposter to try it out:


POST /imposters HTTP/1.1
Host: localhost:9497
Accept: application/json
Content-Type: application/json

{
  "port": 4546,
  "protocol": "http",
  "name": "proxy",
  "stubs": [
    {
      "responses": [{ "inject": "function (request, state) {\n    var count = state.requests ? Object.keys(state.requests).length : 0,\n        util = require('util');\n\n    return {\n        body: util.format('There have been %s proxied calls', count)\n    };\n}" }],
      "predicates": [{
        "equals": {
          "method": "GET",
          "path": "/counter"
        }
      }]
    },
    {
      "responses": [{ "inject": "function (request, state, logger, callback) {\n    var cacheKey = request.method + ' ' + request.path;\n\n    if (typeof state.requests === 'undefined') {\n        state.requests = {};\n    }\n\n    if (state.requests[cacheKey]) {\n        logger.info('Using previous response');\n        callback(state.requests[cacheKey]);\n    }\n\n    var http = require('http'),\n        options = {\n            method: request.method,\n            hostname: 'localhost',\n            port: 5555,\n            path: request.path,\n            headers: request.headers\n        },\n        httpRequest = http.request(options, function (response) {\n            var body = '';\n            response.setEncoding('utf8');\n            response.on('data', function (chunk) {\n                body += chunk;\n            });\n            response.on('end', function () {\n                var stubResponse = {\n                        statusCode: response.statusCode,\n                        headers: response.headers,\n                        body: body\n                    };\n                logger.info('Successfully proxied: ' + JSON.stringify(stubResponse));\n                state.requests[cacheKey] = stubResponse;\n                callback(stubResponse);\n            });\n        });\n    httpRequest.end();\n}" }]
    }
  ]
}

The first stub uses the following function. It depends on the second stub to set a variable on the state parameter each time it proxies, and returns a count of proxied calls. Note that it uses the exact same requests field that the origin server injection uses on the state parameter, but for a different purpose. This is OK - state is shared between stubs on one imposter, but not shared between imposters. This function requires in node's util module, showing an example that takes advantage of the runtime environment.


function (request, state) {
    var count = state.requests ? Object.keys(state.requests).length : 0,
        util = require('util');

    return {
        body: util.format('There have been %s proxied calls', count)
    };
}

The second stub uses an asynchronous proxy with a cache:


function (request, state, logger, callback) {
    var cacheKey = request.method + ' ' + request.path;

    if (typeof state.requests === 'undefined') {
        state.requests = {};
    }

    if (state.requests[cacheKey]) {
        logger.info('Using previous response');
        callback(state.requests[cacheKey]);
    }

    var http = require('http'),
        options = {
            method: request.method,
            hostname: 'localhost',
            port: 5555,
            path: request.path,
            headers: request.headers
        },
        httpRequest = http.request(options, function (response) {
            var body = '';
            response.setEncoding('utf8');
            response.on('data', function (chunk) {
                body += chunk;
            });
            response.on('end', function () {
                var stubResponse = {
                        statusCode: response.statusCode,
                        headers: response.headers,
                        body: body
                    };
                logger.info('Successfully proxied: ' + JSON.stringify(stubResponse));
                state.requests[cacheKey] = stubResponse;
                callback(stubResponse);
            });
        });
    httpRequest.end();
}

Our first request should trigger a proxy call:


GET /first HTTP/1.1
Host: localhost:4546

HTTP/1.1 200 OK
Content-Type: application/json
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

{
  "count": 1
}

A second call also proxies because the path is different.


GET /second HTTP/1.1
Host: localhost:4546

HTTP/1.1 200 OK
Content-Type: application/json
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

{
  "count": 2
}

The third request should be served from the cache of the proxy imposter:


GET /first HTTP/1.1
Host: localhost:4546

HTTP/1.1 200 OK
Content-Type: application/json
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

{
  "count": 1
}

Finally, let's query the other stub on the proxy imposter:


GET /counter HTTP/1.1
Host: localhost:4546

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

There have been 2 proxied calls

Since this was in the same imposter, the dry run validation did not cause unexpected results.

This is the most complicated example in the documentation, and took mountebank some time to get it working (these examples in the docs are executed as part of the test suite). This is appropriate since injection is the most complicated thing you can do in the tool. When injection errors are detected, mountebank logs the full eval'd code, including the bits he's added on top of your code. He also logs JSON representation of both the request (or request field) and the state parameters to the function. mountebank made heavy use of the debug logs while developing this example.

As always, if you get stuck, ask for help.