Mountebank: Multi-Protocol Service Virtualization for Testing

Mountebank: Multi-Protocol Service Virtualization for Testing

Most API mocking tools only speak HTTP. That's fine for REST APIs, but modern systems include gRPC services, legacy TCP protocols, SMTP servers, and binary protocols that HTTP-only tools can't touch.

Mountebank is a service virtualization tool that handles multiple protocols through a unified interface. You configure virtual services called "imposters" over HTTP, and Mountebank creates mock servers that speak whatever protocol you need — HTTP, HTTPS, TCP, or SMTP.

What Is Service Virtualization?

Service virtualization differs from API mocking in scope. API mocking typically means replacing a single HTTP endpoint in a unit or integration test. Service virtualization creates stand-ins for entire external services — complete with realistic network behavior, protocol handling, and stateful interactions.

Mountebank provides:

  • Imposters: Virtual server instances running on a port, speaking a protocol
  • Stubs: Request/response pairs that define how the imposter responds
  • Behaviors: Post-processing on responses (delays, decorators, shell transforms)
  • Predicates: Rules for matching incoming requests to stubs

Installation

npm install -g mountebank
# Start Mountebank on port 2525 (admin port)
mb

Or with Docker:

docker run -p 2525:2525 -p 3000:3000 bbyars/mountebank

Mountebank runs as a persistent daemon. You interact with it via HTTP calls to the admin port (2525 by default).

Creating an HTTP Imposter

An imposter is a virtual server. You create it by POSTing to Mountebank's admin API:

curl -X POST http://localhost:2525/imposters \
  -H 'Content-Type: application/json' \
  -d <span class="hljs-string">'{
    "port": 3000,
    "protocol": "http",
    "stubs": [
      {
        "responses": [
          {
            "is": {
              "statusCode": 200,
              "headers": { "Content-Type": "application/json" },
              "body": "{\"id\": 1, \"name\": \"Alice\", \"email\": \"alice@example.com\"}"
            }
          }
        ],
        "predicates": [
          {
            "equals": {
              "method": "GET",
              "path": "/users/1"
            }
          }
        ]
      }
    ]
  }'

Now GET http://localhost:3000/users/1 returns the mocked response.

Stubs and Predicates

Stubs are ordered — Mountebank tries each stub's predicates in sequence and uses the first matching stub.

Predicate types

{
  "stubs": [
    {
      "predicates": [
        {
          "equals": {
            "method": "POST",
            "path": "/users",
            "headers": { "Content-Type": "application/json" }
          }
        }
      ],
      "responses": [{ "is": { "statusCode": 201, "body": "{\"id\": 2}" } }]
    },
    {
      "predicates": [
        {
          "contains": {
            "path": "/users"
          }
        }
      ],
      "responses": [{ "is": { "statusCode": 200, "body": "[]" } }]
    }
  ]
}

Available predicates: equals, deepEquals, contains, startsWith, endsWith, matches (regex), exists, not, or, and, inject (JavaScript function).

Predicate on request body

{
  "predicates": [
    {
      "equals": {
        "method": "POST",
        "path": "/users"
      }
    },
    {
      "contains": {
        "body": "alice@example.com"
      }
    }
  ],
  "responses": [
    {
      "is": {
        "statusCode": 409,
        "body": "{\"error\": \"Email already exists\"}"
      }
    }
  ]
}

Dynamic Responses with JavaScript

For responses that need to compute values based on the request, use inject:

{
  "responses": [
    {
      "inject": "function(request, state, logger) { var id = request.path.split('/').pop(); return { statusCode: 200, body: JSON.stringify({ id: parseInt(id), name: 'User ' + id }) }; }"
    }
  ]
}

This lets you return different responses based on path parameters, request bodies, or any custom logic.

Stateful responses

Mountebank maintains a state object across requests within an imposter. Use this to simulate stateful APIs:

{
  "stubs": [
    {
      "predicates": [{ "equals": { "method": "POST", "path": "/counter" } }],
      "responses": [
        {
          "inject": "function(request, state, logger) { state.count = (state.count || 0) + 1; return { statusCode: 200, body: JSON.stringify({ count: state.count }) }; }"
        }
      ]
    },
    {
      "predicates": [{ "equals": { "method": "GET", "path": "/counter" } }],
      "responses": [
        {
          "inject": "function(request, state, logger) { return { statusCode: 200, body: JSON.stringify({ count: state.count || 0 }) }; }"
        }
      ]
    }
  ]
}

TCP Imposters

For services that speak raw TCP (legacy mainframes, IoT devices, custom protocols):

{
  "port": 5000,
  "protocol": "tcp",
  "mode": "text",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "data": "ACK\n"
          }
        }
      ],
      "predicates": [
        {
          "startsWith": {
            "data": "PING"
          }
        }
      ]
    }
  ]
}

For binary protocols, use mode: "binary" and encode data as base64:

{
  "port": 5001,
  "protocol": "tcp",
  "mode": "binary",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "data": "AAEC"
          }
        }
      ]
    }
  ]
}

HTTPS Imposters

Mountebank can terminate TLS for HTTPS services:

{
  "port": 3001,
  "protocol": "https",
  "key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
  "cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
  "stubs": [
    {
      "responses": [{ "is": { "statusCode": 200, "body": "Secure response" } }]
    }
  ]
}

For testing, you can use a self-signed certificate and configure your client to ignore certificate errors.

Response Behaviors

Behaviors modify responses after the stub matches. They add realism to your virtual services.

Adding latency

{
  "responses": [
    {
      "is": { "statusCode": 200, "body": "{}" },
      "behaviors": [
        {
          "wait": 500
        }
      ]
    }
  ]
}

For variable latency:

{
  "behaviors": [
    {
      "wait": "Math.floor(Math.random() * 1000) + 200"
    }
  ]
}

Shell transform

Execute a shell command and use its output as the response:

{
  "behaviors": [
    {
      "shellTransform": "node transform.js"
    }
  ]
}

This lets you use external scripts for complex response generation.

Repeating responses

Serve the same response N times, then move to the next one. Useful for simulating retries:

{
  "stubs": [
    {
      "responses": [
        {
          "is": { "statusCode": 503 },
          "behaviors": [{ "repeat": 2 }]
        },
        {
          "is": { "statusCode": 200, "body": "{\"status\": \"ok\"}" }
        }
      ]
    }
  ]
}

The first two calls get 503 (simulating a service recovering). The third and subsequent calls get 200.

Managing Imposters Programmatically

Listing all imposters

curl http://localhost:2525/imposters

Getting a specific imposter

curl http://localhost:2525/imposters/3000

This returns the imposter definition including all requests received — useful for verifying that your tests actually hit the right stubs.

Deleting imposters

# Delete a specific imposter
curl -X DELETE http://localhost:2525/imposters/3000

<span class="hljs-comment"># Delete all imposters
curl -X DELETE http://localhost:2525/imposters

Saving imposters to file

# Save current imposters to a file
curl http://localhost:2525/imposters > imposters.json

<span class="hljs-comment"># Restore imposters from file
mb --configfile imposters.json

Docker Integration

For CI environments, run Mountebank in Docker alongside your application:

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    depends_on:
      - mountebank
    environment:
      - API_URL=http://mountebank:3000
    ports:
      - "8080:8080"

  mountebank:
    image: bbyars/mountebank
    ports:
      - "2525:2525"
      - "3000:3000"
    volumes:
      - ./mocks:/mocks
    command: mb --configfile /mocks/imposters.json

With this setup, your application calls http://mountebank:3000 instead of the real API, and the docker-compose network handles routing.

Using Mountebank in Test Suites

Node.js with Jest

Use the mountebank npm package to control Mountebank from within tests:

npm install --save-dev mountebank
// test/setup.js
const mb = require('mountebank')

let mbServer

beforeAll(async () => {
  mbServer = await mb.create({ port: 2525, pidfile: 'mb.pid' })
  
  // Create an imposter
  await mbServer.post('/imposters', {
    port: 3000,
    protocol: 'http',
    stubs: [
      {
        responses: [
          { is: { statusCode: 200, body: JSON.stringify({ users: [] }) } }
        ]
      }
    ]
  })
})

afterAll(async () => {
  await mbServer.del('/imposters')
  await mbServer.stop()
})

Loading imposter definitions from files

Keep imposter definitions in JSON files and load them in test setup:

const fs = require('fs')
const path = require('path')

const imposters = JSON.parse(
  fs.readFileSync(path.join(__dirname, 'mocks/imposters.json'), 'utf-8')
)

beforeAll(async () => {
  await mbServer.post('/imposters', imposters.userService)
  await mbServer.post('/imposters', imposters.paymentService)
})

Recording Real API Traffic

Mountebank can record real API responses and save them as stub definitions — the same approach as Hoverfly's capture mode:

{
  "port": 3000,
  "protocol": "http",
  "stubs": [],
  "record": {
    "host": "api.real-service.com",
    "port": 443,
    "mode": "record"
  }
}

Make requests through Mountebank, then save the imposter to capture all recorded stubs:

curl http://localhost:2525/imposters/3000 > recorded-stubs.json

This creates a replay file you can use in CI without depending on the real API.

When to Use Mountebank

Mountebank shines in specific scenarios:

Multi-protocol systems: If you're testing services that mix HTTP, TCP, and legacy protocols, Mountebank handles all of them through one admin interface.

Contract testing at the network level: Mountebank validates that your application sends the right requests and handles realistic responses — testing the full HTTP stack, not just the JavaScript layer.

QA environment setup: A Mountebank server can stand in for multiple external dependencies simultaneously, giving QA a stable test environment that doesn't depend on third-party APIs.

Simulating failure modes: The repeat behavior and inject functions let you simulate partial outages, cascading failures, and recovery scenarios.

For simple REST API mocking in browser tests, MSW or Mockoon are lighter. But when your system involves multiple protocols or you need stateful, networked virtual services, Mountebank is the right tool.

Conclusion

Mountebank treats service virtualization as a first-class concern. By modeling external services as imposters with protocol awareness, stateful behavior, and JavaScript scripting, it handles scenarios that simpler mocking tools can't.

The initial learning curve — configuring imposters via JSON and the admin API — pays off in testing complex distributed systems where multiple services need to be virtualized simultaneously. For teams running microservices architectures or working with legacy TCP/binary protocols, Mountebank fills a gap that HTTP-only mocking tools leave open.

Read more