DEV Community

Cover image for To Mock or Not to Mock: A Practical Guide to Mock Servers For APIs
BRUNO SOUZA
BRUNO SOUZA

Posted on

To Mock or Not to Mock: A Practical Guide to Mock Servers For APIs

When you're building a backend that integrates with multiple third-party services, such as marketplaces, payment gateways, shipping providers, and 3PLs, your test suite quickly becomes a hostage to availability, rate limits, and unpredictable responses. I hit this problem hard while working on an integration platform that connected to external partners.

In this article, I'll walk through what mock testing is, why it matters, and give a practical overview of three language-agnostic tools: Postman Mock Server, WireMock, and the Pact Stub Server.

This is the first article in a three-part series:

  • Part 1 (this article): Language-agnostic tools — Postman, WireMock, and Pact Stub Server
  • Part 2: PHP-specific tools — Phiremock, HTTP Mock, and Mock Webserver
  • Part 3: Node.js-specific tools — Nock and MSW

The tools in this article don't care what language your application is written in. Whether you're running PHP, Node.js, Java, Python, or Go — these tools will work because they operate at the HTTP level, not at the code level. They're also the best choice when multiple services in different languages all need to share the same set of mocks.


What is Mock Testing?

Mock testing is the practice of emulating external services so your application can be tested against any scenario in complete isolation, without touching the real service.

The key advantages are that developers and testers get expected results without depending on a real (possibly unstable) external service, there's no risk of polluting production or staging data on third-party systems, you have full control over responses to simulate timeouts, 500 errors, malformed payloads, and edge cases, and tests run faster with no real network overhead.

The goal, in practical terms, is to intercept every outbound HTTP call your application makes and return a pre-defined, predictable response. Your code doesn't know the difference, and that's exactly the point.


Tools at a Glance

Tool Type How it runs Best for
Postman Mock Server Cloud-based GUI Cloud (no setup) Prototyping, team sharing, frontend collaboration
WireMock HTTP stub server Docker / standalone JAR Powerful integration test environments
Pact Stub Server HTTP stub server Binary / Docker Serving pre-written pact JSON files as a live mock

Postman Mock Server

If your team already uses Postman for API documentation and manual testing, spinning up a mock server from an existing collection is remarkably quick — and since it runs in the cloud, any service in any language can call it.

How to Create One

Step 1 — Add an example to a request.
Open a request inside a Collection, send it once to capture a real response, click the ... menu to the right of the request, and choose Add Example. Postman pre-fills it with the actual response you received.

Creating a Postman Mock Server

Step 2 — Create the mock server.
Go to the Collection settings and click + New next to the Mock Servers section. Set the mock server name, pick an environment (e.g. API_LOCAL), optionally check Save the mock server URL as an environment variable, and click Create Mock Server.

Postman - Creating Mock Server in Collection

Showing Mock Server in the Collection

Postman generates a unique URL like https://xxxxxxxx.mock.pstmn.io. Any request matching your saved examples will return the mocked response. In your application config, swap the base URL to point to the testing URL. No code changes needed.

Postman - Mock Server Details

Simulating Different Scenarios

You can add multiple examples to the same request, each with a different response body or status code. Postman selects the matching example based on query params, headers, or path.

For example, having two examples for GET /api/products/:id — one returning 200 with a product, another returning 404 with an error — lets different consumers test both paths just by hitting different URLs or passing different headers.

Pros & Cons

Pros: Zero configuration code, excellent GUI, language-independent (any service can call it), great for sharing mocks across teams, free tier available.

Cons: Responses must be captured from a real endpoint first — you can't fabricate a response that doesn't yet exist without manually editing the example body. Limited dynamic manipulation compared to code-based tools. Requires a Postman account.


WireMock

WireMock is the battle-tested standalone HTTP mock server originally from the Java ecosystem — but it's fully language-agnostic and works perfectly from any project. You run it as a Docker container or a standalone JAR, and interact with it via a REST API or one of its official client libraries for Node.js, Python, Go, and others.

It's the right choice when you need a powerful, long-lived mock server shared across multiple services in a Docker Compose environment, or when you need advanced features like stateful scenarios and response templating.

Starting WireMock via Docker

docker run -d \
  --name wiremock \
  -p 8080:8080 \
  -v $PWD/wiremock:/home/wiremock \
  wiremock/wiremock:latest
Enter fullscreen mode Exit fullscreen mode

Once running, WireMock exposes http://localhost:8080 for your application to call, and http://localhost:8080/__admin for its management API.

Defining Stubs via the REST API

You can create stubs with a plain HTTP POST — any language, any tool:

curl -X POST http://localhost:8080/__admin/mappings \
  -H "Content-Type: application/json" \
  -d '{
    "request": {
      "method": "GET",
      "url": "/api/products/1"
    },
    "response": {
      "status": 200,
      "body": "{\"id\": 1, \"name\": \"Gift Box\", \"price\": 49.99}",
      "headers": { "Content-Type": "application/json" }
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Stub Files (Declarative Approach)

Alternatively, drop JSON files into ./wiremock/mappings/ and they are loaded on startup — great for version-controlling your mock definitions alongside your code:

{
  "request": {
    "method": "POST",
    "urlPattern": "/api/orders",
    "bodyPatterns": [
      {
        "matchesJsonPath": "$.productId"
      }
    ],
    "headers": {
      "Authorization": { "matches": "Bearer .+" }
    }
  },
  "response": {
    "status": 201,
    "jsonBody": { "orderId": "abc-123" },
    "headers": { "Content-Type": "application/json" },
    "fixedDelayMilliseconds": 200
  }
}
Enter fullscreen mode Exit fullscreen mode

Stateful Scenarios

One of WireMock's most powerful features is scenarios — mocks that change state based on how many times they've been called. This is perfect for simulating async workflows.

{
  "scenarioName": "Order Processing",
  "requiredScenarioState": "Started",
  "newScenarioState": "Processing",
  "request": {
    "method": "POST",
    "url": "/api/orders"
  },
  "response": {
    "status": 202,
    "jsonBody": { "status": "processing" }
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "scenarioName": "Order Processing",
  "requiredScenarioState": "Processing",
  "request": {
    "method": "GET",
    "url": "/api/orders/abc-123"
  },
  "response": {
    "status": 200,
    "jsonBody": { "status": "completed" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Response Templating

WireMock can also extract data from the request and inject it into the response — useful when your code echoes back submitted data:

{
  "request": {
    "method": "POST",
    "url": "/api/products"
  },
  "response": {
    "status": 201,
    "body": "{\"id\": \"{{randomValue type='UUID'}}\", \"name\": \"{{{jsonPath request.body '$.name'}}}\"}",
    "headers": { "Content-Type": "application/json" },
    "transformers": ["response-template"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

Pros: Extremely feature-rich (stateful scenarios, response templating, fault simulation, proxying); runs in Docker so it's shareable across a whole team or CI environment; battle-tested at enterprise scale; excellent documentation.

Cons: Requires a running Docker container or a Java process — more infrastructure overhead than in-process solutions; initial learning curve with the mapping DSL; the stub format is verbose for simple cases.


Pact Stub Server

The Pact Stub Server is a standalone HTTP mock server that serves responses defined in pact-format JSON files. You write your interactions (request/response pairs) as JSON, point the stub server at them, and it starts serving those responses immediately — no code required, no language SDK to install.

It's built in Rust, distributed as a single binary or a Docker image, and fully language-agnostic. The stub server is conceptually similar to WireMock's declarative mapping files, but uses the Pact V4 specification JSON format instead.

Note: This is not the same as Pact contract testing. The stub server simply serves pre-written responses — there is no consumer/provider verification involved. Contract testing is a separate topic we'll cover in a future article.

Installation

Via Docker (recommended):

docker pull pactfoundation/pact-stub-server
Enter fullscreen mode Exit fullscreen mode

Via binary download:

Download the latest release for your platform from the GitHub releases page (supports Linux, macOS, and Windows on both x86_64 and arm64).

Writing a Pact Stub File

The stub server reads standard pact JSON files. You can write these by hand — they're just JSON files describing which request should return which response:

{
  "consumer": { "name": "ProductService" },
  "provider": { "name": "ProductAPI" },
  "interactions": [
    {
      "description": "get product 1",
      "request": {
        "method": "GET",
        "path": "/api/products/1"
      },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "id": 1,
          "name": "Gift Box",
          "price": 49.99
        }
      }
    },
    {
      "description": "get unknown product",
      "request": {
        "method": "GET",
        "path": "/api/products/999"
      },
      "response": {
        "status": 404,
        "headers": { "Content-Type": "application/json" },
        "body": { "error": "Not found" }
      }
    },
    {
      "description": "create an order",
      "request": {
        "method": "POST",
        "path": "/api/orders",
        "headers": { "Content-Type": "application/json" }
      },
      "response": {
        "status": 201,
        "headers": { "Content-Type": "application/json" },
        "body": { "orderId": "abc-123", "status": "processing" }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Save this as pacts/ProductService-ProductAPI.json.

Running the Stub Server

From a single pact file:

pact-stub-server --file pacts/ProductService-ProductAPI.json --port 8080
Enter fullscreen mode Exit fullscreen mode

From an entire directory of pact files:

pact-stub-server --dir pacts/ --port 8080
Enter fullscreen mode Exit fullscreen mode

Via Docker, mounting a local pacts folder:

docker run -t -p 8080:8080 \
  -v "$(pwd)/pacts:/app/pacts" \
  pactfoundation/pact-stub-server \
  -p 8080 -d /app/pacts
Enter fullscreen mode Exit fullscreen mode

Once running, your application points to http://localhost:8080 instead of the real service. The stub server matches incoming requests by method, path, and query parameters, then returns the matching response body and headers.

Docker Compose Integration

For a shared dev/test environment, add the stub server alongside your application services:

services:
  app:
    build: .
    environment:
      PRODUCT_API_URL: http://stub:8080
    depends_on:
      - stub

  stub:
    image: pactfoundation/pact-stub-server
    ports:
      - "8080:8080"
    volumes:
      - ./pacts:/app/pacts
    command: ["-p", "8080", "-d", "/app/pacts"]
Enter fullscreen mode Exit fullscreen mode

Serving Multiple Scenarios with Provider States

If your application needs the same endpoint to return different responses depending on the test scenario (e.g. an existing product vs. an out-of-stock one), use provider states. Each interaction can declare a providerState, and you filter which state the stub server uses at runtime.

Add multiple interactions for the same endpoint with different provider states in your pact file:

{
  "consumer": { "name": "ProductService" },
  "provider": { "name": "ProductAPI" },
  "interactions": [
    {
      "description": "get product 1 - in stock",
      "providerState": "product 1 is in stock",
      "request": { "method": "GET", "path": "/api/products/1" },
      "response": {
        "status": 200,
        "body": { "id": 1, "name": "Gift Box", "available": true }
      }
    },
    {
      "description": "get product 1 - out of stock",
      "providerState": "product 1 is out of stock",
      "request": { "method": "GET", "path": "/api/products/1" },
      "response": {
        "status": 200,
        "body": { "id": 1, "name": "Gift Box", "available": false }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Start the stub server filtered to a specific state:

pact-stub-server --dir pacts/ --port 8080 --provider-state "in stock"
Enter fullscreen mode Exit fullscreen mode

Or, if you need the server to handle multiple states in a single run, pass the desired state as a request header. Start the server with --provider-state-header-name:

pact-stub-server --dir pacts/ --port 8080 \
  --provider-state-header-name "X-Provider-State"
Enter fullscreen mode Exit fullscreen mode

Then your test sends the header to tell the stub which interaction to match:

# Returns the "in stock" response
curl -H "X-Provider-State: product 1 is in stock" http://localhost:8080/api/products/1

# Returns the "out of stock" response
curl -H "X-Provider-State: product 1 is out of stock" http://localhost:8080/api/products/1
Enter fullscreen mode Exit fullscreen mode

Watch Mode

During development, pact files change frequently. The --watch flag tells the stub server to monitor files and reload automatically without a restart:

pact-stub-server --dir pacts/ --port 8080 --watch
Enter fullscreen mode Exit fullscreen mode

Any time you add, edit, or delete a pact file in the watched directory, the stub server picks up the changes immediately.

Loading Pacts from a URL or Pact Broker

The stub server can also fetch pact files from remote sources, which is useful in CI environments:

# Load from a URL
pact-stub-server --url https://your-server/pacts/ProductService-ProductAPI.json --port 8080

# Load from a Pact Broker (authenticated)
pact-stub-server \
  --broker-url https://your-pact-broker \
  --token your-bearer-token \
  --provider-name ProductAPI \
  --port 8080
Enter fullscreen mode Exit fullscreen mode

CORS Support

For browser-based consumers (e.g. a React app calling the stub during development), enable CORS with the -o flag:

pact-stub-server --dir pacts/ --port 8080 --cors
Enter fullscreen mode Exit fullscreen mode

This automatically responds to OPTIONS preflight requests and adds the Access-Control-Allow-Origin: * header to all responses.

Pros & Cons

Pros: Single binary with zero runtime dependencies — no JVM, no Node.js, no PHP needed; language-agnostic (any service can call it); JSON stub files are plain text and version-control friendly; watch mode makes development smooth; can pull pacts directly from a Pact Broker in CI; multi-platform Docker image available.

Cons: Matching is limited to method, path, and query params — no body-matching or conditional response logic like WireMock offers; response templating (injecting request data into responses) is not supported; designed primarily for prototyping rather than complex integration test scenarios.


Summary

The three tools in this article sit at different points on the effort/power spectrum. Postman gets you a shareable mock in minutes with no code, making it ideal for early prototyping and frontend collaboration. WireMock gives you the full power of a production-grade stub server with stateful scenarios and response templating, making it the right choice for complex integration test environments. The Pact Stub Server sits between the two — a single binary driven entirely by plain JSON files, with no infrastructure overhead and zero language dependencies.

In practice, they're not mutually exclusive: Postman works great during early development when the API isn't stable yet, the Pact Stub Server is a lightweight option for dev and CI environments where you want file-based stub definitions, and WireMock handles scenarios that need more sophisticated matching or stateful behaviour.


This is Part 1 of a three-part series on mock servers. Continue with Part 2: PHP Tools and Part 3: Node.js Tools. (To be published soon)

Article based on an internal presentation I gave at a previous company. If you have questions or want to share how your team approaches this problem, drop a comment below!

— Bruno Souza | Senior Software Engineer | LinkedIn

Top comments (0)