mountebank

mountebank - over the wire test doubles


the apothecary

Predicates

In the absence of a predicate, a stub always matches, and there's never a reason to add more than one stub to an imposter. Predicates allow imposters to have much richer behavior by defining whether or not a stub matches a request. When multiple stubs are created on an imposter, the first stub that matches is selected.

Each predicate object contains one or more of the request fields as keys. Predicates are added to a stub in an array, and all predicates are AND'd together. The following predicate operators are allowed:

Operator Description
equals The request field matches the predicate
deepEquals Performs nested set equality on the request field, useful when the request field is an object (e.g. the query field in http)
contains The request field contains the predicate
startsWith The request field starts with the predicate
endsWith The request field ends with the predicate
matches The request field matches the JavaScript regular expression defined with the predicate.
exists If true, the request field must exist. If false, the request field must not exist.
not Inverts a predicate
or Logically or's two predicates together
and Logically and's two predicates together
inject Injects JavaScript to decide whether the request matches or not. See the injection page for more details.

Predicates can be parameterized. mountebank accepts the following predicate parameters:

Parameter Default Description
caseSensitive false Determines if the match is case sensitive or not. This includes keys for objects such as query parameters.
except "" Defines a regular expression that is stripped out of the request field before matching.
xpath null Defines an object containing a selector string and, optionally, an ns object field that defines a namespace map. The predicate's scope is limited to the selected value in the request field.
jsonpath null Defines an object containing a selector string. The predicate's scope is limited to the selected value in the request field.

See the equals example below to see the caseSensitive and except parameters in action. See the xpath page for xpath examples. See the jsonpath page for jsonpath examples.

Almost all predicates are scoped to a request field; see the protocol pages linked to from the sidebar to see the request fields. inject is the sole exception. It takes a string function that accepts the entire request. See the injection page for details.

The predicates work intuitively for base64-encoded binary data as well by internally converting the base64-encoded string to a JSON string representing the byte array. For example, sending "AQIDBA==" will get translated to "[1,2,3,4]", and predicates expecting "AgM=" will get translated to "[2,3]". Even though "AQIDBA==" does not contain "AgM=", a contains predicate will match, because "[1,2,3,4]" does contain "[2,3]". This works well for everything but matches, because any regular expression operators get encoded as binary. mountebank recommends that you stay away from matches if you're dealing in binary. In mountebank's experience, contains is the most useful predicate for binary imposters, as even binary RPC data generally contains strings representing method names.

Matching arrays

On occasion you may encounter multi-valued keys. This can be the case with querystrings and HTTP headers that have repeating keys, for example ?key=first&key=second. In those cases, deepEquals will require all the values (in any order) to match. All other predicates will match if any value matches, so an equals predicate will match with the value of second in the example above.

JSON predicates are also allowed, for example, to match HTTP bodies. When the body contains an array, the logic above still applies: deepEquals requires all values to match, and other predicates will match if any value in the array matches.

You also have the option of specifying an array in the predicate definition. If you do so, then all fields in the predicate array must match (in any order). Most predicates will match even if there are additional fields in the actual request array; deepEquals requires the array lengths to be equal. The following example shows this with a querystring:

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

{
  "port": 3333,
  "protocol": "http",
  "stubs": [
    {
      "predicates": [{
        "deepEquals": {
          "query": { "key": ["first", "second"] }
        }
      }],
      "responses": [{
        "is": {
          "body": "Entire array matched"
        }
      }]
    },
    {
      "predicates": [{
        "equals": {
          "query": { "key": ["first", "second"] }
        }
      }],
      "responses": [{
        "is": {
          "body": "Subset of array matched"
        }
      }]
    },
    {
      "predicates": [{
        "equals": {
          "query": { "key": "first" }
        }
      }],
      "responses": [{
        "is": {
          "body": "A field in the array matched"
        }
      }]
    }
  ]
}

First let's call the imposter matching both keys in the deepEquals predicate. For it to match, no other keys must be present, although the order does not matter.

GET /path?key=second&key=first HTTP/1.1
Host: localhost:3333

Since both keys match and there are no extraneous keys, we get our expected response from the first stub.

HTTP/1.1 200 OK
Connection: close
Date: Sat, 06 May 2017 02:30:31 GMT
Transfer-Encoding: chunked

Entire array matched

If we add a third key not specified by the predicate, it no longer matches the deepEquals predicate. It does, however, match the equals predicate, which supports matching a subset of arrays.

GET /path?key=second&key=first&key=third HTTP/1.1
Host: localhost:3333

Since both keys match, we get our expected response from the first stub.

HTTP/1.1 200 OK
Connection: close
Date: Sat, 06 May 2017 02:30:31 GMT
Transfer-Encoding: chunked

Subset of array matched

If the request is missing either array value specified in the predicate, it no longer matches.

GET /path?key=first&key=third HTTP/1.1
Host: localhost:3333

In this case, our third stub matches, because it does not use an array predicate, and one of the actual array values in the request matches the predicate definition

HTTP/1.1 200 OK
Connection: close
Date: Sat, 06 May 2017 02:30:31 GMT
Transfer-Encoding: chunked

A field in the array matched

Matching XML or JSON

mountebank has special support for matching XML and JSON request fields, such as in an http body or tcp data field. Where XML or JSON predicates are used against string fields, mountebank will attempt to parse the field as XML or JSON and apply the given predicate. If he is unable to parse the field, the predicate will fail; otherwise it will pass or fail according to the selected value.

See the xpath page for xpath examples.

See the json page for json examples.

Examples

The examples below use both HTTP and TCP imposters. The TCP examples use netcat (nc) to send TCP data over a socket, which is like telnet, but makes the output easier to script. The examples for binary imposters use the base64 command line tool to decode base64 to binary before sending to the socket.

equals

Let's create an HTTP imposter with multiple stubs:

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

{
  "port": 4545,
  "protocol": "http",
  "stubs": [
    {
      "responses": [{ "is": { "statusCode": 400 } }],
      "predicates": [
        {
          "equals": {
            "method": "POST",
            "path": "/test",
            "query": {
              "first": "1",
              "second": "2"
            },
            "headers": {
              "Accept": "text/plain"
            }
          }
        },
        {
          "equals": { "body": "hello, world" },
          "caseSensitive": true,
          "except": "!$"
        }
      ]
    },
    {
      "responses": [{ "is": { "statusCode": 406 } }],
      "predicates": [{ "equals": { "headers": { "Accept": "application/xml" } } }]
    },
    {
      "responses": [{ "is": { "statusCode": 405 } }],
      "predicates": [{ "equals": { "method": "PUT" } }]
    },
    {
      "responses": [{ "is": { "statusCode": 500 } }],
      "predicates": [{ "equals": { "method": "PUT" } }]
    }
  ]
}

The first predicate is the most complex, and the request has to match all of the specified request fields. We have the option of putting multiple fields under one equals predicate or splitting each one into a separate predicate in the array. In this example, all of the ones that share the default predicate parameters are together. For those, neither the case of the keys nor the values will affect the outcome. The body predicate is treated separately. The text will be compared in a case-sensitive manner, after stripping away the regular expression !$ (an exclamation mark anchored to the end of the string).

The order of the query parameters and header fields does not matter.

POST /test?Second=2&First=1 HTTP/1.1
Host: localhost:4545
accept: text/plain

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

The second stub matches if the header changes.

POST /test?Second=2&First=1 HTTP/1.1
Host: localhost:4545
Accept: application/xml

"hello, world!"
HTTP/1.1 406 Not Acceptable
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

The third stub matches on a PUT.

PUT /test?Second=2&First=1 HTTP/1.1
Host: localhost:4545
Accept: application/json

"hello, world!"

HTTP/1.1 405 Method Not Allowed
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

The fourth stub will never run, since it matches the same requests as the third stub. mountebank always chooses the first stub that matches based on the order you add them to the stubs array when creating the imposter.

deepEquals

Let's create an HTTP imposter with multiple stubs:

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

{
  "port": 4556,
  "protocol": "http",
  "stubs": [
    {
      "responses": [{ "is": { "body": "first" } }],
      "predicates": [{
        "deepEquals": {
          "query": {}
        }
      }]
    },
    {
      "responses": [{ "is": { "body": "second" } }],
      "predicates": [{
        "deepEquals": {
          "query": {
            "first": "1"
          }
        }
      }]
    },
    {
      "responses": [{ "is": { "body": "third" } }],
      "predicates": [{
        "deepEquals": {
          "query": {
            "first": "1",
            "second": "2"
          }
        }
      }]
    }
  ]
}

The first predicate matches only a request without a querystring.

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

first

The second stub matches only if the exact querystring is sent.

GET /test?First=1 HTTP/1.1
Host: localhost:4556
HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

second

The third stub matches only if both query keys are sent.

GET /test?Second=2&First=1 HTTP/1.1
Host: localhost:4556
HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

third

Any additional query parameters will trigger the default HTTP response.

GET /test?Second=2&First=1&Third=3 HTTP/1.1
Host: localhost:4556
HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked
contains

Let's create a binary TCP imposter with multiple stubs:

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

{
  "port": 4547,
  "protocol": "tcp",
  "mode": "binary",
  "stubs": [
    {
      "responses": [{ "is": { "data": "Zmlyc3QgcmVzcG9uc2U=" } }],
      "predicates": [{ "contains": { "data": "AgM=" } }]
    },
    {
      "responses": [{ "is": { "data": "c2Vjb25kIHJlc3BvbnNl" } }],
      "predicates": [{ "contains": { "data": "Bwg=" } }]
    },
    {
      "responses": [{ "is": { "data": "dGhpcmQgcmVzcG9uc2U=" } }],
      "predicates": [{ "contains": { "data": "Bwg=" } }]
    }
  ]
}

We're sending a base64-encoded version of four bytes: 0x1, 0x2, 0x3, and 0x4. Our first predicate is a base64 encoded version of 0x2 and 0x3. The response is a base64-encoded version of the string "first response":

echo 'AQIDBA==' | base64 --decode | nc localhost 4547
first response

Next we'll send 0x5, 0x6, 0x7, and 0x8, matching on a predicate encoding 0x7 and 0x8:

echo 'BQYHCA==' | base64 --decode | nc localhost 4547
second response

The third stub will never run, since it matches the same requests as the second stub. mountebank always chooses the first stub that matches based on the order you add them to the stubs array when creating the imposter.

startsWith

Let's create a text-based imposter with multiple stubs. Binary imposters won't see any interesting behavior difference with only startsWith predicates:

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

{
  "port": 4548,
  "protocol": "tcp",
  "mode": "text",
  "stubs": [
    {
      "responses": [{ "is": { "data": "first response" } }],
      "predicates": [{ "startsWith": { "data": "first" } }]
    },
    {
      "responses": [{ "is": { "data": "second response" } }],
      "predicates": [{ "startsWith": { "data": "second" } }]
    },
    {
      "responses": [{ "is": { "data": "third response" } }],
      "predicates": [{ "startsWith": { "data": "second" } }]
    }
  ]
}

The match is not case-sensitive:

echo 'FIRST REQUEST' | nc localhost 4548
first response

The same is true for the second stub.

echo 'Second Request' | nc localhost 4548
second response

The third stub will never run, since it matches the same requests as the second stub. mountebank always chooses the first stub that matches based on the order you add them to the stubs array when creating the imposter.

endsWith

Let's create a binary-based imposter with multiple stubs:

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

{
  "port": 4549,
  "protocol": "tcp",
  "mode": "binary",
  "stubs": [
    {
      "responses": [{ "is": { "data": "Zmlyc3QgcmVzcG9uc2U=" } }],
      "predicates": [{ "endsWith": { "data": "AwQ=" } }]
    },
    {
      "responses": [{ "is": { "data": "c2Vjb25kIHJlc3BvbnNl" } }],
      "predicates": [{ "endsWith": { "data": "BQY=" } }]
    },
    {
      "responses": [{ "is": { "data": "dGhpcmQgcmVzcG9uc2U=" } }],
      "predicates": [{ "endsWith": { "data": "BQY=" } }]
    }
  ]
}

We'll use the command line base64 tool to decode the request to binary before sending to the imposter. We're sending a base64-encoded version of four bytes: 0x1, 0x2, 0x3, and 0x4. Our first predicate is a base64 encoded version of 0x3 and 0x4. The response is a base64-encoded version of the string "first response":

echo 'AQIDBA==' | base64 --decode | nc localhost 4549
first response

Next we'll send 0x1, 0x2, 0x4, 0x5, and 0x6, matching on a predicate encoding 0x5 and 0x6:

echo 'AQIDBAUG' | base64 --decode | nc localhost 4549
second response

The third stub will never run, since it matches the same requests as the second stub. mountebank always chooses the first stub that matches based on the order you add them to the stubs array when creating the imposter.

matches

Let's create a text-based imposter with multiple stubs. Binary imposters cannot use the matches predicate.

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

{
  "port": 4550,
  "protocol": "tcp",
  "mode": "text",
  "stubs": [
    {
      "responses": [{ "is": { "data": "first response" } }],
      "predicates": [{
        "matches": { "data": "^first\\Wsecond" },
        "caseSensitive": true
      }]
    },
    {
      "responses": [{ "is": { "data": "second response" } }],
      "predicates": [{ "matches": { "data": "second\\s+request" } }]
    },
    {
      "responses": [{ "is": { "data": "third response" } }],
      "predicates": [{ "matches": { "data": "second\\s+request" } }]
    }
  ]
}

The first stub requires a case-sensitive match on a string starting with "first", followed by a non-word character, followed by "second":

echo 'first second' | nc localhost 4550
first response

The second stub is not case-sensitive.

echo 'Second Request' | nc localhost 4550
second response

The third stub will never run, since it matches the same requests as the second stub. mountebank always chooses the first stub that matches based on the order you add them to the stubs array when creating the imposter.

exists

The exists predicate is primarily for object data types, like HTTP headers and query parameters. It works on string fields by simply returning true if the exists value is true and the string if non-empty. Setting the exists value to false inverts the meaning.

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

{
  "port": 4551,
  "protocol": "http",
  "stubs": [
    {
      "responses": [{ "is": { "body": "first response" } }],
      "predicates": [
        {
          "exists": {
            "query": {
              "q": true,
              "search": false
            },
            "headers": {
              "Accept": true,
              "X-Rate-Limit": false
            }
          }
        }
      ]
    },
    {
      "responses": [{ "is": { "body": "second response" } }],
      "predicates": [
        {
          "exists": {
            "method": true,
            "body": false
          }
        }
      ]
    },
    {
      "responses": [{ "is": { "body": "third response" } }],
      "predicates": [
        {
          "exists": { "body": true }
        }
      ]
    }
  ]
}

The first stub matches if the querystring includes q, but not if it includes search, and if the headers include Accept, but not if they include X-Rate-Limit.

GET /?q=mountebank HTTP/1.1
Host: localhost:4551
Accept: text/plain
HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

first response

The second stub matches if the request method is a non-empty string (always true), and if the body is empty.

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

second response

The last stub matches if the body is non-empty:

POST / HTTP/1.1
Host: localhost:4551

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

third response
not

The not predicate negates its child predicate.

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

{
  "port": 4552,
  "protocol": "tcp",
  "mode": "text",
  "stubs": [
    {
      "responses": [{ "is": { "data": "not test" } }],
      "predicates": [{ "not": { "equals": { "data": "test\n" } } }]
    },
    {
      "responses": [{ "is": { "data": "test" } }],
      "predicates": [{ "equals": { "data": "test\n" } }]
    }
  ]
}

The first stub matches if the is sub-predicate does not match:

echo 'production' | nc localhost 4552
not test

As expected, the second stub matches if the is sub-predicate does match:

echo 'test' | nc localhost 4552
test
or

The or predicate matches if any of its sub-predicates match.

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

{
  "port": 4553,
  "protocol": "tcp",
  "mode": "text",
  "stubs": [
    {
      "responses": [{ "is": { "data": "matches" } }],
      "predicates": [
        {
          "or": [
            { "startsWith": { "data": "start" } },
            { "endsWith": { "data": "end\n" } },
            { "contains": { "data": "middle" } }
          ]
        }
      ]
    }
  ]
}

The stub matches if the first sub-predicate matches:

echo 'start data transmission' | nc localhost 4553
matches

The stub matches if the second sub-predicate matches:

echo 'data transmission end' | nc localhost 4553
matches

The stub matches if the last sub-predicate matches:

echo 'data middle transmission' | nc localhost 4553
matches

The stub does not match none of the sub-predicates match...

echo 'data transmission' | nc localhost 4553

...which yields an empty response:


and

The and predicate matches if all of its sub-predicates match.

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

{
  "port": 4554,
  "protocol": "tcp",
  "mode": "text",
  "stubs": [
    {
      "responses": [{ "is": { "data": "matches" } }],
      "predicates": [
        {
          "and": [
            { "startsWith": { "data": "start" } },
            { "endsWith": { "data": "end\n" } },
            { "contains": { "data": "middle" } }
          ]
        }
      ]
    }
  ]
}

The first request matches all sub-predicates, triggering the stub response:

echo 'start middle end' | nc localhost 4554
matches

The stub matches two of the three sub-predicates, which is not enough to match the and predicate.

The stub does not match none of the sub-predicates match...

echo 'start end' | nc localhost 4554

No response is sent.


inject

The inject predicate allows you to inject JavaScript to determine if the predicate should match or not. The JavaScript should be a function that accepts the request object (and optionally a logger) and returns true or false. See the injection page for details.

The execution will have access to a node.js environment. The following example uses node's Buffer object to decode base64 to a byte array.

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

{
  "port": 4555,
  "protocol": "tcp",
  "mode": "binary",
  "stubs": [
    {
      "responses": [{ "is": { "data": "Zmlyc3QgcmVzcG9uc2U=" } }],
      "predicates": [{
        "inject": "function (request, logger) { logger.info('Inside injection'); return Buffer.from(request.data, 'base64')[2] > 100; }"
      }]
    },
    {
      "responses": [{ "is": { "data": "c2Vjb25kIHJlc3BvbnNl" } }],
      "predicates": [{
        "inject": "request => { return Buffer.from(request.data, 'base64')[2] <= 100; }"
      }]
    }
  ]
}

The first stub matches if the third byte is greater than 100. The request we're sending is an encoding [99, 100, 101]:

echo 'Y2Rl' | base64 --decode | nc localhost 4555
first response

The logs will also show the injected log output. The second predicate has to match a request originating from localhost with the third byte less than or equal to 100. We're sending [98, 99, 100]:

echo 'YmNk' | base64 --decode | nc localhost 4555

...giving the response:

second response