mountebank

mountebank - over the wire test doubles


the apothecary

Stub Behaviors

You can alter the response created by adding to the behaviors array, which acts as a middleware pipeline of transformations to the response. At the moment, mountebank accepts the following behaviors:

Behavior Description
wait Adds latency to a response by waiting a specified number of milliseconds before sending the response.

Tip: Setting the addWaitBehavior flag on proxies will automatically add this behavior with the actual time it took to call the downstream service

copy Copies one or more values from request fields into the response. You can tokenize the response and select values from request fields using regular expressions, xpath, or jsonpath.
lookup Queries an external data source for data based on a key selected from the request. Like the copy behavior, you can tokenize the response and select the key from the request using regular expressions, xpath, or jsonpath.
decorate Post-processes the response using JavaScript injection before sending it. Post-processing opens up a world of opportunities - you can use a decorate behavior to add data to a proxied response or substitute data from the request into the response, for example. The value passed into the decorate behavior is a JavaScript function that can take up to three values: the request, the response, and a logger. You can either mutate the response passed in (and return nothing), or return an altogether new response.

Tip: Setting the addDecorateBehavior flag on proxies will automatically add this function as decorate behavior on the generated responses

The --allowInjection command line flag must be set to support this behavior

shellTransform Like decorate, a shellTransform post-processes the response, but instead of using JavaScript injection, it shells out to another application. That application will get two command line parameters representing the request JSON and the response JSON, and should print to stdout the transformed response JSON.

The --allowInjection command line flag must be set to support this behavior.

Multiple behaviors can be added to a response, and they will be executed in array order. While each object in the array may contain only one type of behavior, you are free to repeat any behavior as many times as you want. For example, take a look at the following response:

{
  "is": " { ... },
  "behaviors": [
    { "copy": { ... } },
    { "decorate": "..." },
    { "lookup": "..." },
    { "shellTransform": "..." },
    { "decorate": "..." },
    { "wait": 500 },
    { "shellTransform": "..." }
  ]
}

The ability to compose multiple behaviors together gives you complete control over the response transformation.

Examples

Select the behavior below for relevant examples, which explore each type of behavior in isolation:

wait
Parameter Type Description
wait A positive integer, or a string If a number is passed in, mountebank will wait that number of milliseconds before returning. If a string is passed in, it is expected to be a parameterless JavaScript function that returns the number of milliseconds to wait.

The --allowInjection command line flag must be set to support passing in a JavaScript function

The wait behavior is conceptually quite simple. Just pass in a number of milliseconds to wait into the behavior:

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

{
  "port": 4545,
  "protocol": "http",
  "stubs": [
    {
      "responses": [
        {
          "is": { "body": "This took at least half a second to send" },
          "behaviors": [
            { "wait": 500 }
          ]
        }
      ]
    }
  ]
}

Now we can call the imposter. But have some patience, it'll take the better portion of a second before you'll get your response...

GET / HTTP/1.1
Host: localhost:4545
HTTP/1.1 200 OK
Connection: close
Date: Thu, 01 Jan 2015 02:30:31 GMT
Transfer-Encoding: chunked

This took at least half a second to send

If a more advanced wait strategy is needed, you can also specify a function body as the wait variable. As long as the function returns a number, mountebank can be configured to wait a variable amount of time.

The --allowInjection command line flag must be set to support using a function to determine the wait time.

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

{
  "port": 4545,
  "protocol": "http",
  "stubs": [
    {
      "responses": [
        {
          "is": { "body": "This took at least 100 to 1000 ms to send" },
          "behaviors": [
            { "wait": "function() { return Math.floor(Math.random() * 901) + 100; }" }
          ]
        }
      ]
    }
  ]
}

The result of the function determines the wait time:

GET / HTTP/1.1
Host: localhost:4545
HTTP/1.1 200 OK
Connection: close
Date: Thu, 01 Jan 2015 02:30:31 GMT
Transfer-Encoding: chunked

This took at least 100 to 1000 ms to send
copy
Parameter Type Description
copy An object An object specifying the request field and response token, as well as a way of selecting the value from the request field
copy.from A string or an object The name of the request field to copy from, or, if the request field is an object, then an object specifying the path to the request field. For example,
{ "from": "body" }
and
{ "from": { "query": "q" } }
are both valid.
copy.into A string The token to replace in the response with the selected request value. There is no need to specify which field in the response the token will be in; all response tokens will be replaced in all response fields. Sometimes, the request selection returns multiple values. In those cases, you can add an index to the token, while the unindexed token represents the first match. For example, if you specify
{ "into": "${NAME}" }
as your token configuration, then both ${NAME} and ${NAME}[0] will be replaced by the first match, ${NAME}[1] will be replaced by the second match, and so on.
copy.using An object The configuration needed to select values from the response
copy.using.method An string The method used to select the value(s) from the request. Allowed values are regex, xpath, and jsonpath.
copy.using.selector An string The selector used to select the value(s) from the request. For a regex, this would be the pattern, and the replacement value will be the entire match. Match groups using parentheses are supported and can be replaced using indexed tokens as described in the copy[].into description. xpath and jsonpath selectors work on XML and JSON documents. If the request value does not match the selector (including through XML or JSON parsing errors), nothing is replaced.
copy.using.ns An object For xpath selectors, the ns object maps namespace aliases to URLs
copy.using.options An object For regex selectors, the options object describes the regular expression options
copy.using.options.ignoreCase A boolean Uses a case-insensitive regular expression
copy.using.options.multiline A boolean Uses a multiline regular expression

The copy behavior supports dynamically replacing values in the response with something that comes from the request. It relies on you adding tokens of your own choosing into the response fields you want replaced. We'll look at the following examples:

Regular expressions

The following example shows multiple regular expression matches on request fields to copy into the response.

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

{
  "port": 8585,
  "protocol": "http",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "statusCode": "${code}",
            "headers": {
              "X-Test": "${header}"
            },
            "body": "The request name was ${name}. Hello, ${name}!"
          },
          "behaviors": [
            {
              "copy": {
                "from": "path",
                "into": "${code}",
                "using": { "method": "regex", "selector": "\\d+" }
              }
            },
            {
              "copy": {
                "from": { "headers": "X-Request" },
                "into": "${header}",
                "using": { "method": "regex", "selector": ".+" }
              }
            },
            {
              "copy": {
                "from": { "query": "name" },
                "into": "${name}",
                "using": {
                  "method": "regex",
                  "selector": "MOUNT\\w+$",
                  "options": { "ignoreCase": true }
                }
              }
            }
          ]
        }
      ]
    }
  ]
}

This example shows off many of the options of the copy behavior. For example, we can plug tokens into any of the response fields (including the statusCode), and it shows how to navigate object request fields, like the name querystring parameter. It shows an example of using regular expressions options to get a case-insensitive regular expression to capture the name query parameter. It also shows matching multiple request fields using an array of copy configurations. Let's see what happens when we craft a request to match all of those selectors:

GET /statusCode/400?ignore=this&name=mountebank HTTP/1.1
Host: localhost:8585
X-REQUEST: Header value
HTTP/1.1 400 Bad Request
X-Test: Header value
Connection: close
Date: Thu, 28 Dec 2016 11:37:31 GMT
Transfer-Encoding: chunked

The request name was mountebank. Hello, mountebank!

xpath

The following example shows a simple namespaced xpath match to grab the first title field in an XML document and copy it into the BOOK response token.

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

{
  "port": 8586,
  "protocol": "http",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "body": "Have you read BOOK?"
          },
          "behaviors": [
            {
              "copy": {
                "from": "body",
                "into": "BOOK",
                "using": {
                  "method": "xpath",
                  "selector": "//isbn:title",
                  "ns": {
                    "isbn": "http://schemas.isbn.org/ns/1999/basic.dtd"
                  }
                }
              }
            }
          ]
        }
      ]
    }
  ]
}

The ns object map is optional and can be ignored if your xpath selector doesn't depend on namespaces. It doesn't matter how many name elements exist in the XML. Without using indexed tokens, only the first match will be used:

POST /names HTTP/1.1
Host: localhost:8586

<books xmlns:isbn="http://schemas.isbn.org/ns/1999/basic.dtd">
  <book>
    <isbn:title>Game of Thrones</isbn:title>
    <isbn:summary>Dragons and political intrigue</isbn:summary>
  </book>
  <book>
    <isbn:title>Harry Potter</isbn:title>
    <isbn:summary>Dragons and a boy wizard</isbn:summary>
  </book>
  <book>
    <isbn:title>The Hobbit</isbn:title>
    <isbn:summary>A dragon and short people</isbn:summary>
  </book>
</books>
HTTP/1.1 200 OK
Connection: close
Date: Thu, 28 Dec 2016 11:37:31 GMT
Transfer-Encoding: chunked

Have you read Game of Thrones?

jsonpath

The following example translates the XML example above into JSON. To make it more interesting, we'll show it using the tcp protocol

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

{
  "port": 8587,
  "protocol": "tcp",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "data": "Have you read BOOK?"
          },
          "behaviors": [
            {
              "copy": {
                "from": "data",
                "into": "BOOK",
                "using": {
                  "method": "jsonpath",
                  "selector": "$..title"
                }
              }
            }
          ]
        }
      ]
    }
  ]
}

Again, by default only the first match will be used:

echo '{
  "books": [
    {
      "title": "Game of Thrones",
      "summary": "Dragons and political intrigue"
    },
    {
      "title": "Harry Potter",
      "summary": "Dragons and a boy wizard"
    },
    {
      "title": "The Hobbit",
      "summary": "A dragon and short people"
    }
  ]
}' | nc localhost 8587
Have you read Game of Thrones?
DELETE /imposters/8587 HTTP/1.1 Host: localhost:48451

Indexed replacements

Finally, let's show an example that uses multiple matches for a given selector. To show that the same approach works for multiple selection methods, we'll show it for both regular expressions and jsonpath:

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

{
  "port": 8588,
  "protocol": "http",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "body": "${BOOK}[1]: ${SUMMARY}[0]\n${BOOK}[2]: ${SUMMARY}[1]\n${BOOK}[3]: ${SUMMARY}[2]"
          },
          "behaviors": [
            {
              "copy": {
                "from": "body",
                "into": "${SUMMARY}",
                "using": {
                  "method": "jsonpath",
                  "selector": "$..summary"
                }
              }
            },
            {
              "copy": {
                "from": { "query": "books" },
                "into": "${BOOK}",
                "using": {
                  "method": "regex",
                  "selector": "([^,]+),([^,]+),(.+)$"
                }
              }
            }
          ]
        }
      ]
    }
  ]
}

Note the mismatched indexes between the two selection methods. This is because we use the standard regular expression semantics around matched groups, which is that the first element in the matches will be the entire matched expression, the second element will be the first parenthesized match group, and so on. Also note that ${SUMMARY}[0] and ${SUMMARY} will be treated identically. We'll trigger the substitutions with the following request:

POST /?books=Game%20of%20Thrones,Harry%20Potter,The%20Hobbit HTTP/1.1
Host: localhost:8588

{
  "books": [
    {
      "title": "Game of Thrones",
      "summary": "Dragons and political intrigue"
    },
    {
      "title": "Harry Potter",
      "summary": "Dragons and a boy wizard"
    },
    {
      "title": "The Hobbit",
      "summary": "A dragon and short people"
    }
  ]
}
HTTP/1.1 200 OK
Connection: close
Date: Thu, 28 Dec 2016 11:37:31 GMT
Transfer-Encoding: chunked

Game of Thrones: Dragons and political intrigue
Harry Potter: Dragons and a boy wizard
The Hobbit: A dragon and short people
lookup
Parameter Type Description
lookup An object An object specifying the key (copied from a request field), the data source, and the response token
lookup.key An object The information on how to select the key from the request.
lookup.key.from A string or an object The name of the request field to select from, or, if the request field is an object, then an object specifying the path to the request field. For example,
{ "from": "body" }
and
{ "from": { "query": "q" } }
are both valid.
lookup.key.using An object The configuration needed to select the key from the response
lookup.key.using.method A string The method used to select the key from the request. Allowed values are regex, xpath, and jsonpath.
lookup.key.using.selector A string The selector used to select the key from the request. For a regex, this would be the pattern, and the replacement value will be the entire match by default. xpath and jsonpath selectors work on XML and JSON documents. If the request value does not match the selector (including through XML or JSON parsing errors), nothing is replaced.
lookup.key.using.ns An object For xpath selectors, the ns object maps namespace aliases to URLs
lookup.key.using.options An object For regex selectors, the options object describes the regular expression options
lookup.key.using.options.ignoreCase A boolean Uses a case-insensitive regular expression
lookup.key.using.options.multiline A boolean Uses a multiline regular expression
lookup.key.index An int (defaults to 0) Each of the selection options returns an array: regex returns an array of parenthesized gropus (with the entire match in the 0th index), and jsonpath and xpath return an array of matches. This field selects the appropriate value from the array to use as the lookup key into the data source.
lookup.fromDataSource An object Configuration for the external data source to lookup data based on the key. Each lookup configuration may only specify one data source.
lookup.fromDataSource.csv An object Configuration for using a CSV file as the data source
lookup.fromDataSource.csv.path A string The path to the CSV file, which must be readable by the mb process
lookup.fromDataSource.csv.keyColumn A string The header of the column to scan for a match against the key. If a match is found, the entire row will be returned.
lookup.fromDataSource.csv.delimiter A string(default to comma ,) The delimiter separated colums in CSV file.
lookup.into A string The token to replace in the response with the selected request value. There is no need to specify which field in the response the token will be in; all response tokens will be replaced in all response fields. A successful match will return a hashmap or dictionary type object, with named indexes. For example, if you specify
{ "into": "${NAME}" }
as your token configuration, and the data source returned a row containing "first" and "last" fields, then ${NAME}["first"] and ${NAME}["last"] will be replaced by the appropriate data. You can quote the field name with double quotes, single quotes, or no quotes at all.

The lookup behavior supports dynamically replacing values in the response with something that comes from an external data source. Looking up the values from the data source requires first selecting the key value from the request. The key selection and replacement behavior in the response mirrors the functionality for the copy behavior. We'll look at the following examples:

Look at the copy examples to see how to do advanced key selection using jsonpath and xpath.

We'll use the following CSV file for these examples, saved as "values.csv" in the current working directory of the mb process (you can always use an absolute path):

State_ID,code,Name,price,tree,jobs
1111111,400,liquid,235651,mango,farmer
9856543,404,solid,54564564,orange,miner
2222222,500,water,12564,pine,shepherd
1234564,200,plasma,2656,guava,lumberjack
9999999,200,lovers,9999,dogwood,steel worker

Lookup up from a CSV file based on a key selected with a regular expression

The following example shows selecting the key using a regular expression to match against the path. We're using the index field to select the first parenthesized group in the regular expression.

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

{
  "port": 9595,
  "protocol": "http",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "statusCode": "${row}['code']",
            "headers": {
              "X-Tree": "${row}['tree']"
            },
            "body": "Hello ${row}['Name'], have you done your ${row}['jobs'] today?"
          },
          "behaviors": [
            {
              "lookup": {
                "key": {
                  "from": "path",
                  "using": { "method": "regex", "selector": "/(.*)$" },
                  "index": 1
                },
                "fromDataSource": {
                  "csv": {
                    "path": "/app/values.csv",
                    "keyColumn": "Name",
                    "delimiter": ","
                  }
                },
                "into": "${row}"
              }
            }
          ]
        }
      ]
    }
  ]
}

As with the copy behavior, we can plug tokens into any of the response fields (including the statusCode). Let's see what happens when we craft a request to match all of those selectors:

GET /liquid HTTP/1.1
Host: localhost:9595
HTTP/1.1 400 Bad Request
X-Tree: mango
Connection: close
Date: Thu, 28 Dec 2016 11:37:31 GMT
Transfer-Encoding: chunked

Hello liquid, have you done your farmer today?
decorate
Parameter Type Description
decorate A string The decorate function, used to transform the response through JavaScript. It can either mutate the response in place or return a new response object.

The --allowInjection command line flag must be set to support this behavior.

The decorate function should take a single parameter, which will contain the following fields:

Field Description
request The entire request object, containing all request fields
response The entire response object
state An initially empty object, scoped to the imposter, that will be shared with predicate and response injection functions. You can use it to capture and mutate shared state.
logger A logger object with debug, info, warn, and error functions to write to the mountebank logs.

The decorate behavior is quite powerful, allowing nearly unlimited (synchronous) post-processing of the response. Since it relies on JavaScript injection, the --allowInjection flag must be passed in to mb on startup.

Here are a couple ideas of what to do with post-processing:

Add the current time to a response

Many people store static imposter files and load them via the --configfile command line switch. The decorate behavior supports an elegant way of adding dynamic data to the responses. Let's add the current timestamp to each response. We'll pass in a stringified version of the following JavaScript function:

(config) => {
    var pad = function (number) {
            return (number < 10) ? '0' + number : number.toString();
        },
        now = new Date(),
        time = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds());

    config.response.body = config.response.body.replace('${TIME}', time);
}
POST /imposters HTTP/1.1
Host: localhost:48451
Accept: application/json
Content-Type: application/json

{
  "port": 5545,
  "protocol": "http",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "body": "The time is ${TIME}"
          },
          "behaviors": [
            { "decorate": "(config) => { var pad = function (number) { return (number < 10) ? '0' + number : number.toString(); }, now = new Date(), time = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()); config.response.body = config.response.body.replace('${TIME}', time); }" }
          ]
        }
      ]
    }
  ]
}

Now we can call the imposter to get the current time.

GET / HTTP/1.1
Host: localhost:5545
HTTP/1.1 200 OK
Connection: close
Date: Thu, 01 Jan 2015 02:30:31 GMT
Transfer-Encoding: chunked

The time is 16:43:02

Add a custom header to a proxied response

Proxying provides considerable power out of the box. However, there are times where you need to augment the proxied response with something else to properly simulate your testing scenario. In this example, we'll proxy to the example above (port 5545) that adds the current time to the response body. We'll augment the proxied response with a custom header. Here's the decorator function, passed into the imposter creation as a string:

(config) => {
    config.response.headers['X-Test'] = 'True';
}
POST /imposters HTTP/1.1
Host: localhost:48451
Accept: application/json
Content-Type: application/json

{
  "port": 7545,
  "protocol": "http",
  "stubs": [
    {
      "responses": [
        {
          "proxy": { "to": "http://localhost:5545" },
          "behaviors": [
            { "decorate": "(config) => { config.response.headers['X-Test'] = 'True'; }" }
          ]
        }
      ]
    }
  ]
}

Now we should get the custom header back:

GET /test HTTP/1.1
Host: localhost:7545
HTTP/1.1 200 OK
Connection: close
Date: Thu, 01 Jan 2015 02:30:31 GMT
Transfer-Encoding: chunked
X-Test: True

The time is 17:16:23
shellTransform
Parameter Type Description
shellTransform A string Represents the path to a command line application. The application should retrieve the JSON-encoded request and response from the environment and print out the transformed response to stdout.

The --allowInjection command line flag must be set to support this behavior.

The shellTransform behavior plays a similar role as the decorate behavior, enabling a programmatic transformation of the response. However, you don't have to write the transformation logic in JavaScript -- it can be in the language of your choice.

Since it allows shelling out to another application, the --allowInjection flag must be passed in to mb on startup.

mountebank will expose the following environment variables to your shell application:

  • MB_REQUEST which contains the JSON request as a string, and
  • MB_RESPONSE which contains the current JSON response as a string

The application should write to stdout a JSON representation of the transformed response.

We'll show a simple example of shelling out to plug in response values based on an external data source.

Using an external data source

At times you may find it convenient to use an external data store to fill in dynamic values based on data coming in from the request. You can combine an is canned response with a shellTransform behavior to achieve this, regardless of what the external data source is. For this example, we'll assume it's a simple pipe-delimited file mapping customer ids to names. We'll save it as names.csv in the directory we run mb from.

Note that, by reformatting the file, this example would more easily be satisfied by the lookup behavior.

123|Frodo Baggins
234|Samwise Gamgee
345|Gandalf the White
456|Smeagol

We expect the incoming http request to specify the id in the URL, and we want to represent the name in the response body. We'll set up our imposter like this:

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

{
  "port": 5555,
  "protocol": "http",
  "stubs": [
    {
      "predicates": [{ "matches": { "path": "/accounts/\\d+" } }],
      "responses": [
        {
          "is": { "body": "Hello, ${YOU}!" },
          "behaviors": [
            { "shellTransform": "node /app/addName.js" }
          ]
        }
      ]
    }
  ]
}

In this example, we're shelling out to a node.js application, which isn't much different than using a decorate function. However, that's just to keep it simple; the application could be written in any language.

Let's create the addName.js file..

var request = JSON.parse(process.env.MB_REQUEST),
    response = JSON.parse(process.env.MB_RESPONSE),
    requestId = request.path.replace('/accounts/', ''),
    fs = require('fs'),
    mappings = fs.readFileSync('/app/names.txt', { encoding: 'utf8' }),
    lines = mappings.split(/\r?\n/);

for (let i = 0; i < lines.length; i += 1) {
    var fields = lines[i].split('|'),
        id = fields[0],
        name = fields[1];

    if (requestId === id) {
        response.body = response.body.replace('${YOU}', name);
    }
}

console.log(JSON.stringify(response));

Now we can test it out:

GET /accounts/234 HTTP/1.1
Host: localhost:5555
HTTP/1.1 200 OK
Connection: close
Date: Wed, 07 Jan 2016 21:27:14 GMT
Transfer-Encoding: chunked

Hello, Samwise Gamgee!

And again...

GET /accounts/456 HTTP/1.1
Host: localhost:5555
HTTP/1.1 200 OK
Connection: close
Date: Wed, 07 Jan 2016 21:27:14 GMT
Transfer-Encoding: chunked

Hello, Smeagol!