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. |
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.
|
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.
|
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:
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 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:35553
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
DELETE /imposters/4545 HTTP/1.1
Host: localhost:35553
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.
POST /imposters HTTP/1.1
Host: localhost:35553
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
DELETE /imposters/4545 HTTP/1.1
Host: localhost:35553
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,
and
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
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:35553
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!
DELETE /imposters/8585 HTTP/1.1
Host: localhost:35553
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:35553
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?
DELETE /imposters/8586 HTTP/1.1
Host: localhost:35553
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:35553
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:35553
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:35553
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
DELETE /imposters/8588 HTTP/1.1
Host: localhost:35553
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,
and
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
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:
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:35553
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?
DELETE /imposters/9595 HTTP/1.1
Host: localhost:35553
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 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:35553
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:35553
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
DELETE /imposters/5545 HTTP/1.1
Host: localhost:35553
DELETE /imposters/7545 HTTP/1.1
Host: localhost:35553
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 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.
mountebank will expose the following environment variables to your shell application:
MB_REQUEST
which contains the JSON request as a string, andMB_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.
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:35553
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!
DELETE /imposters/5555 HTTP/1.1
Host: localhost:35553