mountebank

mountebank - over the wire test doubles

Fork me on GitHub

the apothecary

Proxies

Proxies are one of the most powerful features of mountebank, rivaled only by the mighty injection. Proxies support record/replay behavior to easily capture a rich set of test data for your test scenarios. Each proxy definition allows you to define the the fields which should be included in newly created predicates.

proxy response types take the following parameters:

Parameter Default Type Description
to required A URL without the path (e.g. http://someserver:3000 or tcp://someserver:3000) Defines the origin server that the request should proxy to.
predicateGenerators [] array An array of objects that defines how the predicates for new stubs are created. Each object in the array defines the fields to generate predicates from. See below for examples.
mode proxyOnce string, one of proxyOnce or proxyAlways. Defines the replay behavior of the proxy. The default proxyOnce mode doesn't require you to explicitly do anything to replay the proxied responses. The proxyAlways mode requires you to run the mb replay command (or equivalent) to switch from record mode to replay mode, but allows a richer set of data to be recorded. See below for details.
injectHeaders {} object Key-value pairs of headers to inject into the proxied request./td>
addWaitBehavior false boolean If true, mountebank will add a wait behavior to the response with the same latency that the proxied call took. This is useful in performance testing scenarios where you want to simulate the actual latency of downstream services that you're virtualizing.
addDecorateBehavior null string, JavaScript If defined, mountebank will add a decorate behavior to the saved response.

http and https proxies add three additional optional parameters for situations where the origin server expects to use mutual authentication and will request a client certificate:

Parameter Default Type Description
cert null A PEM-formatted string The SSL client certificate
key null A PEM-formatted string The SSL client private key
ciphers ALL A valid cipher (see this page for formats) For older (and insecure) https servers, this field allows you to override the cipher used to commuicate

It is occasionally useful to capture how long the original proxied request takes. mountebank stores the number of milliseconds for the request in the _proxyResponseTime field in the response. Setting the addWaitBehavior flag will add that latency to the saved response.

Note, if you use a corporate proxy, then the standard shell http_proxy or https_proxy environment variables will be honored.

Select the behavior of the proxy below for a relevant example:

Recording a rich set of test data through proxying requires also capturing the appropriate predicates from the request, so that saved responses are only replayed when the requests are similar. The predicateGenerators field defines the template for the generated predicates. Each object in the predicateGenerators array takes the following fields:

Parameter Default Type Description
matches {} object The fields that need to be equal in subsequent requests to replay the saved response. Set the field value true to generate a predicate based on it. Nested fields, as in JSON fields or HTTP headers, are supported as well, as long as the leaf keys have a true value. If you set the parent object key (e.g. query) to true, the generated predicate will use deepEquals, requiring the entire object graph to match.
caseSensitive false boolean Determines if the match is case sensitive or not. This includes keys for objects such as query parameters.
except "" string Defines a regular expression that is stripped out of the request field before matching.
xpath null object 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 object Defines an object containing a selector string. The predicate's scope is limited to the selected value in the request field.

With the exception of matches, the fields correspond to the standard predicate parameters. Each object in the predicateGenerators array generates an object in the newly created stub's predicates array. You can decide how strictly you want the generated predicates to match object fields. The following example matches the root query field in its entirety:

"stubs": [{
  "responses": [{
    "proxy": {
      "to": "http://origin-server.com",
      "predicateGenerators": [{
        "matches": { "query": true },
        "caseSensitive": true
      }]
    }
  }]
}]

This will generate a deepEquals predicate at the same level, which requires that all keys and values in the querystring match (although the order can be different). We added the caseSensitive parameter, which will also require the cases of the query keys and values to match. If the incoming request is to /test?q=mountebank&page=1, the following stub will be generating (the saved response is elided for clarity):

"stubs": [
  {
    "predicates": [{
      "caseSensitive": true,
      "deepEquals": {
        "query": {
          "q": "mountebank",
          "page": "1"
        }
      }
    }],
    "responses": [{
      "is": { ... }
    }]
  },
  {
    "responses": [{
      "proxy": {
        "to": "http://origin-server.com",
        "predicateGenerators": [{
          "matches": { "query": true },
          "caseSensitive": true
        }]
      }
    }]
  }
]

Just as with predicates, we could have also specified only the query key we cared about:

"stubs": [{
  "responses": [{
    "proxy": {
      "to": "http://origin-server.com",
      "predicateGenerators": [{
        "matches": {
          "query": { "q": "mountebank" }
        }
      }]
    }
  }]
}]

The same request to /test?q=mountebank&page=1 now generates a more limited predicate:

"stubs": [
  {
    "predicates": [{
      "equals": {
        "query": { "q": "mountebank" }
      }
    }],
    "responses": [{
      "is": { ... }
    }]
  },
  {
    "responses": [{
      "proxy": {
        "to": "http://origin-server.com",
        "predicateGenerators": [{
          "matches": {
            "query": { "q": "mountebank" }
          }
        }]
      }
    }]
  }
]

The xpath and jsonpath predicate parameters work by limiting the scope of the generated predicate to the matching value in the proxied request. If the selector matches multiple values in the proxied request, they will all be part of the generated predicate, using standard predicate array matching rules. For example, following stub will look for number tags in the XML body:

"stubs": [{
  "responses": [{
    "proxy": {
      "to": "http://origin-server.com",
      "predicateGenerators": [{
        "matches": { "body": true },
        "xpath": { "selector": "//number" }
      }]
    }
  }]
}]

We'll send it the following XML body:

<doc>
  <number>1</number>
  <number>2</number>
  <number>3</number>
</doc>

Rather than matching the entire body, the generated predicate will require all three values matching the xpath selector in the original proxied request to be present (in any order):

"stubs": [
  {
    "predicates": [{
      "xpath": { "selector": "//number" },
      "deepEquals": { "body": ["1", "2", "3"] }
    }],
    "responses": [{
      "is": { ... }
    }]
  },
  {
    "responses": [{
      "proxy": {
        "to": "http://origin-server.com",
        "predicateGenerators": [{
          "matches": { "body": true },
          "xpath": { "selector": "//number" }
        }]
      }
    }]
  }
]

Similarly, the following stub looks for number elements in JSON:

"stubs": [{
  "responses": [{
    "proxy": {
      "to": "http://origin-server.com",
      "predicateGenerators": [{
        "matches": { "body": true },
        "jsonpath": { "selector": "$..number" }
      }]
    }
  }]
}]

We'll send it the following JSON body:

[
  { "number": 1 },
  { "number": 2 },
  { "number": 3 }
]

Again, the generated predicate gets scoped to the jsonpath matches.

"stubs": [
  {
    "predicates": [{
      "jsonpath": { "selector": "$..number" },
      "deepEquals": { "body": [1, 2, 3] }
    }],
    "responses": [{
      "is": { ... }
    }]
  },
  {
    "responses": [{
      "proxy": {
        "to": "http://origin-server.com",
        "predicateGenerators": [{
          "matches": { "body": true },
          "jsonpath": { "selector": "$..number" }
        }]
      }
    }]
  }
]

The mode defines the behavior of the proxy.

  • proxyOnce - ensures that the same request (defined by the predicates) is never proxied twice. mountebank only records one response for each request, and automatically replays that response the next time the request predicates match.
  • proxyAlways - All calls will be proxied, allowing multiple responses to be saved for the same logical request. You have to explicitly tell mountebank to replay those responses.

proxyOnce

The default proxyOnce mode is simpler; it always creates a new stub in front of the stub with the proxy response, relying on mountebank's first-match policy to automatically replay the saved response in the new stub the next time a request matches the predicates. Imagine the following stubs array, set by us when we create the imposter:

"stubs": [{
  "responses": [{
    "proxy": {
      "to": "http://origin-server.com",
      "mode": "proxyOnce",
      "predicateGenerators": [{ "matches": { "path": true } }]
    }
  }]
}]

When we issue an HTTP call to /test, the stub will proxy all of the request details to http://origin-server.com/test, and save off the response in a new stub in front of the stub with the proxy response:

"stubs": [
  {
    "predicates": [{ "deepEquals": { "path": "/test" } } ],
    "responses": [{
      "is": {
        "body": "Downstream service response",
        ...
      }
    }]
  },
  {
    "responses": [{
      "proxy": {
        "to": "http://localhost:2001",
        "mode": "proxyOnce",
        "predicateGenerators": [{ "matches": { "path": true } }]
      }
    }]
  }
]

Because of mountebank's first-match policy on stubs, the next time the imposter receives a request to /test, the saved predicates on the newly created stub will match, and the recorded response will be replayed. If the imposter receives a call to /different-path, then it will proxy again, creating a new stub, because the path is different.

proxyAlways

The proxyAlways mode saves stubs behind the proxy stub. This allows you to record a richer set of interactions with the origin server because it can record multiple responses for the same logical request (as defined by the predicates). The consequence is that it requires you to save off the imposter representation and remove or reorder the proxy stubs to replay those interactions. The easiest way to do that is with the mb replay command.

Let's say you had the following stubs array:

"stubs": [{
  "responses": [{
    "proxy": {
      "to": "http://origin-server.com",
      "mode": "proxyAlways",
      "predicateGenerators": [{ "matches": { "path": true } }]
    }
  }]
}]

Every time we send a request to /test, it will be proxied to http://origin-server.com/test. The first time we do that, just like the proxyOnce example, it will add a new stub. The difference is that the new stub will be created at the end of the array:

"stubs": [
  {
    "responses": [
      {
        "proxy": {
          "to": "http://origin-server.com",
          "mode": "proxyAlways",
          "predicateGenerators": [{ "matches": { "path": true } }]
        }
      }
    ]
  },
  {
    "predicates": [{ "deepEquals": { "path": "/test" } }],
    "responses": [{
      "is": {
        "body": "Request number 1",
        ...
      }
    }]
  }
]

Because the proxy occurs before the new stub, the next request will continue to use the proxy response. This allow us to capture multiple responses for the same logical request. If we once again send a request to the /test path, since a stub with the predicate already exists, the response will be added to the existing stub:

"stubs": [
  {
    "responses": [
      {
        "proxy": {
          "to": "http://origin-server.com",
          "mode": "proxyAlways",
          "predicateGenerators": [{ "matches": { "path": true } }]
        }
      }
    ]
  },
  {
    "predicates": [{ "deepEquals": { "path": "/test" } }],
    "responses": [
      {
        "is": {
          "body": "Request number 1",
          ...
        }
      },
      {
        "is": {
          "body": "Request number 2",
          ...
        }
      }
    ]
  }
]

This configuration allows you to capture as rich a set of data as your downstream system provides and play it back with the appropriate predicates. The only additional complexity with proxyAlways is that you have to explicitly switch to replay mode. Conceptually, replaying is as simple as removing the proxies. The easiest way to do that is by using the mb replay command, passing in the port if you're using a nonstandard port for running mb. If, for example, we had previously started mountebank on port 3535, we could switch to replay mode with the following call:

mb replay --port 3535

As always, there's more than one way to do it. You can instead use the API, retrieving all imposters with a call to GET /imposters?replayabe=true&removeProxies=true and sending the payload in a call PUT /imposters.

Now if we look at the imposter configuration, you'll notice that the proxy has been removed. Only the saved responses will be used.

"stubs": [
  {
    "predicates": [{ "deepEquals": { "path": "/test" } }],
    "responses": [
      {
        "is": {
          "body": "Request number 1",
          ...
        }
      },
      {
        "is": {
          "body": "Request number 2",
          ...
        }
      }
    ]
  }
]

The injectHeaders field allows you to modify the request headers before passing them on to the downstream service. To demonstrate, let's create a mirror imposter so we can see what headers are sent. This imposter just takes the request headers it receives and sends them back in the response:

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

{
  "port": 7002,
  "protocol": "http",
  "name": "Mirror",
  "stubs": [{
    "responses": [{
      "is": { "body": "The body." },
      "_behaviors": {
        "decorate": "function (req, res) { res.headers = req.headers; }"
      }
    }]
  }]
}

Now let's set up another imposter that will proxy requests to the mirror imposter. We'll also set up the inject headers field to insert some custom headers in the outgoing request:

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

{
  "port": 7001,
  "protocol": "http",
  "name": "Inject Headers",
  "stubs": [{
    "responses": [{
      "proxy": {
        "to": "http://localhost:7002",
        "injectHeaders": {
           "X-My-Custom-Header-One": "my first value",
           "X-My-Custom-Header-Two": "my second value"
        }
      }
    }]
  }]
}

Then we send a request to our proxy imposter:

GET / HTTP/1.1
Host: localhost:7001
HTTP/1.1 200 OK
Accept: application/json
Host: localhost:7002
Connection: keep-alive
X-My-Custom-Header-One: my first value
X-My-Custom-Header-Two: my second value
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

The body.

Now we can see that the X-My-Custom-Header-One and Two headers were returned back to us, reflected back from the mirror imposter after being injected into the outgoing request.

Every response saves the time it took to call the downstream service in the _proxyResponseTime field. You can tell mountebank to automatically translate that into a wait behavior, which will add the same latency to the saved response. This is a useful technique during performance testing, where you want to virtualize downstream services but still want realistic latencies.

Let's create the following proxy configuration:

"stubs": [{
  "responses": [{
    "proxy": {
      "to": "http://origin-server.com",
      "addWaitBehavior": true
    }
  }]
}]

Next we'll send a request to our proxy imposter to create a new saved response:

GET / HTTP/1.1
Host: origin-server.com

If you look at the imposter configuration, you'll notice the new wait behavior. The value will be the same as the _proxyResponseTime field, which means that the saved response will add the same latency as the real service.

"stubs": [
  {
    "predicates": [],
    "responses": [{
      "is": {
        "statusCode": 200,
        ...
        "_proxyResponseTime": 219
      },
      "_behaviors": {
        "wait": 219
      }
    }]
  },
  {
    "responses": [{
      "proxy": {
        "to": "http://origin-server.com",
        "addWaitBehavior": true,
        "mode": "proxyOnce"
      }
    }]
  }
]

A decorate behavior on a proxy response affects the proxy response itself, not the saved response that the proxy creates. If you want to add a decorate behavior on the saved response, you do so with the addDecorateBehavior field.

We'll decorate our saved responses with the following JavaScript function:

function (request, response) {
  response.body = response.body + ' DECORATED!';
}

Add that function in the addDecorateBehavior field of the proxy configuration:

"stubs": [{
  "responses": [{
    "proxy": {
      "to": "http://origin-server.com",
      "addDecorateBehavior": "function (request, response) { response.body = response.body + ' DECORATED!'; }"
    }
  }]
}]

Next we'll send a request to our proxy imposter to create a new saved response. Notice that the decorator doesn't affect the proxied response (to do that, we'd have to add the same function as a decorate behavior on the proxy response too):

GET / HTTP/1.1
Host: origin-server.com
HTTP/1.1 200 OK
Connection: close
Date: Fri, 19 May 2017 19:39:02 GMT
Transfer-Encoding: chunked

downstream service response

If you look at the imposter configuration, you'll notice the new decorate behavior.

"stubs": [
  {
    "predicates": [],
    "responses": [{
      "is": {
        "body": "downstream service response",
        ...
      },
      "_behaviors": {
        "decorate": "function (request, response) { response.body = response.body + ' DECORATED!'; }"
      }
    }]
  },
  {
    "responses": [{
      "proxy": {
        "to": "http://origin-server.com",
        "addDecorateBehavior": "function (request, response) { response.body = response.body + ' DECORATED!'; }",
        "mode": "proxyOnce"
      }
    }]
  }
]