Skip to content
Published on

[DevOps] Complete Guide to Stub and Mocking Servers: From Concepts to Practice

Authors

1. What Are Test Doubles

In software testing, a Test Double is a generic term for any object that replaces a real dependency component. According to Martin Fowler's classification, Test Doubles fall into five categories.

1.1 Dummy

An object passed to fill parameters but never actually used. It exists solely to satisfy method signatures.

// Dummy example: a logger that's never called
public class DummyLogger implements Logger {
    @Override
    public void log(String message) {
        // does nothing
    }
}

1.2 Stub

A Stub is a Test Double that returns pre-determined responses. It returns fixed data for calls and is used for State Verification.

// Stub example: always returns a fixed user
public class StubUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        return new User(1L, "testuser", "test@example.com");
    }
}

Key characteristics:

  • Returns only pre-programmed responses
  • Does not verify call count or order
  • Verification is done by checking the result state after the test

1.3 Mock

A Mock is a Test Double that verifies the calls themselves. It focuses on Behavior Verification -- whether a specific method was called with specific arguments and how many times.

// Mock example using Mockito
@Test
void shouldSendWelcomeEmail() {
    EmailService mockEmailService = mock(EmailService.class);
    UserService userService = new UserService(mockEmailService);

    userService.registerUser("test@example.com");

    // Behavior verification: check sendWelcomeEmail called exactly once
    verify(mockEmailService, times(1))
        .sendWelcomeEmail("test@example.com");
}

Key characteristics:

  • Set expectations for expected calls in advance
  • Verify call count, order, and arguments
  • Test fails if calls don't match expectations

1.4 Fake

A Fake is a lightweight implementation that actually works. Not used in production, but functional enough for testing purposes.

// Fake example: In-Memory database
public class FakeUserRepository implements UserRepository {
    private final Map<Long, User> store = new HashMap<>();
    private long sequence = 1L;

    @Override
    public User save(User user) {
        user.setId(sequence++);
        store.put(user.getId(), user);
        return user;
    }

    @Override
    public User findById(Long id) {
        return store.get(id);
    }

    @Override
    public List<User> findAll() {
        return new ArrayList<>(store.values());
    }
}

Representative Fake examples:

  • In-memory databases like H2
  • Local filesystem-based storage
  • In-memory message queues

1.5 Spy

A Spy wraps a real object and records call information. It performs actual operations while simultaneously logging call details.

// Mockito Spy example
@Test
void spyExample() {
    List<String> realList = new ArrayList<>();
    List<String> spyList = spy(realList);

    spyList.add("hello");  // element is actually added

    verify(spyList).add("hello");  // verify call record
    assertEquals(1, spyList.size());  // verify actual behavior
}

1.6 Comparison Summary

TypeBehaviorVerificationExample
DummyNone (parameter filler)NoneEmpty implementation
StubReturns fixed responseState verificationHardcoded response
MockSets call expectationsBehavior verificationMockito mock
FakeReal lightweight implState verificationIn-memory DB
SpyReal behavior + recordingBehavior + StateMockito spy

2. Why You Need Mocking Servers

When testing services that depend on external APIs in a microservices architecture, the following problems arise:

  • External service instability: If the external API goes down, your tests fail too
  • Rate limits: External API call limits prevent test repetition
  • Cost: Calling paid APIs for every test generates costs
  • Speed: Network latency increases test time
  • Edge case reproduction: Difficult to reproduce specific error responses, timeouts, etc.
  • Development environment independence: Must be able to develop without external services

Mocking servers solve all these problems by providing the same interface as the actual external service while returning predictable responses.

┌──────────────┐      ┌──────────────┐      ┌───────────────┐
│  Test Code   │─────>│  My Service  │─────>│ Mocking Server │
│              │      │              │      │ (WireMock etc) │
└──────────────┘      └──────────────┘      └───────────────┘
                                           Returns pre-defined responses

3. Major Mocking Server Tools

3.1 WireMock

WireMock is the most powerful and feature-rich open-source Mock server. While Java-based, it can also run as a standalone server.

Key features:

  • JSON-based stub mapping files
  • Powerful Request Matching (URL, headers, body, query parameters)
  • Response Templating (Handlebars-based)
  • Record/Playback mode (records actual API traffic)
  • Stateful Behavior (scenario-based state transitions)
  • Fault Injection (delay, connection drop simulation)
  • Docker image available

Running WireMock with Docker:

docker run -d --name wiremock \
  -p 8080:8080 \
  -v $(pwd)/stubs:/home/wiremock \
  wiremock/wiremock:latest \
  --global-response-templating \
  --verbose

Basic Stub Mapping File Structure:

stubs/mappings/get-user.json:

{
  "request": {
    "method": "GET",
    "urlPathPattern": "/api/users/[0-9]+",
    "headers": {
      "Accept": {
        "contains": "application/json"
      }
    }
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "id": 1,
      "name": "John Doe",
      "email": "john@example.com"
    }
  }
}

Request Matching Patterns:

{
  "request": {
    "method": "POST",
    "urlPath": "/api/orders",
    "bodyPatterns": [
      {
        "matchesJsonPath": "$.items[?(@.quantity > 0)]"
      },
      {
        "matchesJsonPath": {
          "expression": "$.customerId",
          "regex": "^[A-Z]{2}[0-9]{6}$"
        }
      }
    ],
    "queryParameters": {
      "status": {
        "equalTo": "active"
      }
    }
  },
  "response": {
    "status": 201,
    "jsonBody": {
      "orderId": "ORD-001",
      "status": "created"
    }
  }
}

Response Templating Example:

{
  "request": {
    "method": "GET",
    "urlPathTemplate": "/api/users/{userId}"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "id": "{{request.pathSegments.[2]}}",
      "requestedAt": "{{now}}",
      "userAgent": "{{request.headers.User-Agent}}"
    },
    "transformers": ["response-template"]
  }
}

Stateful Behavior (Scenarios):

{
  "mappings": [
    {
      "scenarioName": "OrderFlow",
      "requiredScenarioState": "Started",
      "newScenarioState": "OrderCreated",
      "request": {
        "method": "POST",
        "urlPath": "/api/orders"
      },
      "response": {
        "status": 201,
        "jsonBody": { "status": "created" }
      }
    },
    {
      "scenarioName": "OrderFlow",
      "requiredScenarioState": "OrderCreated",
      "request": {
        "method": "GET",
        "urlPath": "/api/orders/1"
      },
      "response": {
        "status": 200,
        "jsonBody": { "status": "processing" }
      }
    }
  ]
}

Record/Playback Mode:

# Proxy to real API and record responses
docker run -d --name wiremock \
  -p 8080:8080 \
  -v $(pwd)/stubs:/home/wiremock \
  wiremock/wiremock:latest \
  --proxy-all="https://api.example.com" \
  --record-mappings

3.2 MockServer

MockServer is a Java-based mock server that offers differentiated features from WireMock.

Key features:

  • Runtime dynamic Expectation configuration (add/modify stubs via API calls)
  • Forward Proxy mode support
  • Request/Response verification API
  • OpenAPI spec-based auto mock

Docker execution:

docker run -d --name mockserver \
  -p 1080:1080 \
  mockserver/mockserver:latest

Setting Expectations (REST API):

curl -X PUT "http://localhost:1080/mockserver/expectation" \
  -H "Content-Type: application/json" \
  -d '{
    "httpRequest": {
      "method": "GET",
      "path": "/api/products",
      "queryStringParameters": {
        "category": ["electronics"]
      }
    },
    "httpResponse": {
      "statusCode": 200,
      "headers": {
        "Content-Type": ["application/json"]
      },
      "body": {
        "type": "JSON",
        "json": "[{\"id\": 1, \"name\": \"Laptop\", \"price\": 999.99}]"
      }
    },
    "times": {
      "unlimited": true
    }
  }'

Setting Expectations with Java Client:

import org.mockserver.client.MockServerClient;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

new MockServerClient("localhost", 1080)
    .when(
        request()
            .withMethod("GET")
            .withPath("/api/products")
            .withQueryStringParameter("category", "electronics")
    )
    .respond(
        response()
            .withStatusCode(200)
            .withHeader("Content-Type", "application/json")
            .withBody("[{\"id\": 1, \"name\": \"Laptop\"}]")
    );

3.3 json-server

json-server is a lightweight Node.js-based REST API server. It provides an instant REST API with just a single JSON file.

Installation and execution:

npm install -g json-server

# Create db.json file
cat > db.json << 'JSONEOF'
{
  "users": [
    { "id": 1, "name": "Alice", "email": "alice@example.com" },
    { "id": 2, "name": "Bob", "email": "bob@example.com" }
  ],
  "posts": [
    { "id": 1, "title": "Hello World", "userId": 1 },
    { "id": 2, "title": "Testing Guide", "userId": 2 }
  ]
}
JSONEOF

# Start server
json-server --watch db.json --port 3001

Auto-generated endpoints:

# Full CRUD support
GET    /users          # List all
GET    /users/1        # Get by ID
POST   /users          # Create
PUT    /users/1        # Full update
PATCH  /users/1        # Partial update
DELETE /users/1        # Delete

# Query features
GET /users?name=Alice              # Filtering
GET /users?_sort=name&_order=asc   # Sorting
GET /users?_page=1&_limit=10       # Pagination
GET /posts?_expand=user            # Relation expansion

Custom routes and middleware:

// server.js
const jsonServer = require('json-server')
const server = jsonServer.create()
const router = jsonServer.router('db.json')
const middlewares = jsonServer.defaults()

// Custom middleware: check auth header
server.use((req, res, next) => {
  if (req.headers.authorization === 'Bearer test-token') {
    next()
  } else {
    res.status(401).json({ error: 'Unauthorized' })
  }
})

server.use(middlewares)

// Custom route
server.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() })
})

server.use(router)
server.listen(3001, () => {
  console.log('JSON Server is running on port 3001')
})

3.4 Prism (Stoplight)

Prism automatically generates a mock server from an OpenAPI (Swagger) specification.

Installation and execution:

npm install -g @stoplight/prism-cli

# Run mock server from OpenAPI spec
prism mock openapi.yaml --port 4010

# Dynamic response generation mode
prism mock openapi.yaml --dynamic --port 4010

# Validation Proxy mode (validates requests/responses in front of real server)
prism proxy openapi.yaml https://api.example.com --port 4010

OpenAPI spec example:

openapi: 3.0.3
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                      example: 1
                    name:
                      type: string
                      example: 'Alice'
                    email:
                      type: string
                      format: email
                      example: 'alice@example.com'
              examples:
                default:
                  value:
                    - id: 1
                      name: 'Alice'
                      email: 'alice@example.com'
        '401':
          description: Unauthorized

Prism prioritizes example values from the spec, and in --dynamic mode generates random data based on the schema.

3.5 mitmproxy

mitmproxy is an HTTP/HTTPS proxy that can intercept and modify actual traffic.

# Install
pip install mitmproxy

# Start intercept proxy
mitmproxy --listen-port 8888

# Script-based response modification
mitmproxy -s modify_response.py
# modify_response.py
import json
from mitmproxy import http

def response(flow: http.HTTPFlow) -> None:
    if "/api/users" in flow.request.pretty_url:
        flow.response.status_code = 200
        flow.response.set_text(json.dumps([
            {"id": 1, "name": "Mocked User"}
        ]))
        flow.response.headers["Content-Type"] = "application/json"

4. Tool Comparison Table

FeatureWireMockMockServerjson-serverPrismmitmproxy
LanguageJavaJavaNode.jsNode.jsPython
Config MethodJSON files/APIAPIJSON fileOpenAPI specPython script
Request MatchingVery powerfulVery powerfulBasicSpec-basedScript-based
Response TemplatingHandlebarsVelocity/MustacheLimitedSpec-basedFlexible
Record/PlaybackYesYesNoNoYes
Stateful BehaviorYes (scenarios)YesYes (CRUD)NoYes (script)
OpenAPI IntegrationPluginYesNoNativeNo
Docker SupportYesYesYesYesYes
Fault InjectionYesYesNoNoYes
HTTPS SupportYesYesNoYesYes
Learning CurveMediumMediumVery LowLowMedium
Best ForGeneral integration testingDynamic API testingFrontend developmentAPI-First developmentDebugging/analysis

5. WireMock in Practice: Docker Compose Environment

5.1 Project Structure

project/
  docker-compose.yaml
  wiremock/
    mappings/
      get-users.json
      post-order.json
      health-check.json
    __files/
      user-list.json
      error-response.json

5.2 Docker Compose Configuration

version: '3.8'
services:
  wiremock:
    image: wiremock/wiremock:latest
    ports:
      - '8080:8080'
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings
      - ./wiremock/__files:/home/wiremock/__files
    command:
      - '--global-response-templating'
      - '--verbose'
      - '--disable-gzip'
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8080/__admin/health']
      interval: 10s
      timeout: 5s
      retries: 3

  my-service:
    build: .
    environment:
      - EXTERNAL_API_URL=http://wiremock:8080
    depends_on:
      wiremock:
        condition: service_healthy

5.3 Various Mapping Examples

Delayed response simulation:

{
  "request": {
    "method": "GET",
    "urlPath": "/api/slow-endpoint"
  },
  "response": {
    "status": 200,
    "fixedDelayMilliseconds": 3000,
    "jsonBody": { "message": "delayed response" }
  }
}

Random delay (jitter):

{
  "request": {
    "method": "GET",
    "urlPath": "/api/unstable"
  },
  "response": {
    "status": 200,
    "delayDistribution": {
      "type": "lognormal",
      "median": 1000,
      "sigma": 0.25
    },
    "jsonBody": { "message": "response with jitter" }
  }
}

Fault Injection (connection reset):

{
  "request": {
    "method": "GET",
    "urlPath": "/api/fault"
  },
  "response": {
    "fault": "CONNECTION_RESET_BY_PEER"
  }
}

Priority setting:

{
  "priority": 1,
  "request": {
    "method": "GET",
    "urlPath": "/api/users/999"
  },
  "response": {
    "status": 404,
    "jsonBody": { "error": "User not found" }
  }
}

6. API Contract Testing

6.1 Pact

Pact is a framework that supports Consumer-Driven Contract Testing.

┌──────────────┐    Generates Pact file    ┌──────────────┐
│   Consumer   │ ────────────────────────> │  Pact Broker │
│  (Frontend)  │                           │              │
└──────────────┘                           └──────┬───────┘
                                           Verifies Pact file
                                           ┌──────▼───────┐
                                           │   Provider   │
                                           │   (Backend)  │
                                           └──────────────┘

Consumer Test (JavaScript):

const { PactV3 } = require('@pact-foundation/pact')

const provider = new PactV3({
  consumer: 'FrontendApp',
  provider: 'UserService',
})

describe('User API', () => {
  it('should return user by ID', async () => {
    await provider
      .given('a user with ID 1 exists')
      .uponReceiving('a request for user 1')
      .withRequest({
        method: 'GET',
        path: '/api/users/1',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: 1,
          name: 'Alice',
          email: 'alice@example.com',
        },
      })

    await provider.executeTest(async (mockServer) => {
      const response = await fetch(`${mockServer.url}/api/users/1`, {
        headers: { Accept: 'application/json' },
      })
      const user = await response.json()
      expect(user.name).toBe('Alice')
    })
  })
})

6.2 Spring Cloud Contract

Spring Cloud Contract is a contract testing tool for the Java/Spring ecosystem.

Contract definition (Groovy DSL):

// contracts/shouldReturnUser.groovy
Contract.make {
    description "should return user by ID"
    request {
        method GET()
        url "/api/users/1"
        headers {
            accept(applicationJson())
        }
    }
    response {
        status OK()
        headers {
            contentType(applicationJson())
        }
        body([
            id: 1,
            name: "Alice",
            email: "alice@example.com"
        ])
    }
}

7. CI/CD Integration

7.1 Testcontainers + WireMock

import org.wiremock.integrations.testcontainers.WireMockContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
class ExternalApiIntegrationTest {

    @Container
    static WireMockContainer wiremock = new WireMockContainer(
        "wiremock/wiremock:latest"
    )
    .withMappingFromResource("mappings/get-user.json");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("external.api.url", wiremock::getBaseUrl);
    }

    @Autowired
    private UserClient userClient;

    @Test
    void shouldFetchUserFromExternalApi() {
        User user = userClient.getUser(1L);
        assertThat(user.getName()).isEqualTo("John Doe");
    }
}

7.2 WireMock in GitHub Actions

name: Integration Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      wiremock:
        image: wiremock/wiremock:latest
        ports:
          - 8080:8080
        options: >-
          --health-cmd "curl -f http://localhost:8080/__admin/health"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 3

    steps:
      - uses: actions/checkout@v4

      - name: Run Integration Tests
        run: |
          ./gradlew integrationTest
        env:
          EXTERNAL_API_URL: http://localhost:8080

7.3 Testcontainers + MockServer

import org.mockserver.client.MockServerClient;
import org.testcontainers.containers.MockServerContainer;
import org.testcontainers.utility.DockerImageName;

@Testcontainers
class MockServerIntegrationTest {

    @Container
    static MockServerContainer mockServer = new MockServerContainer(
        DockerImageName.parse("mockserver/mockserver:latest")
    );

    @BeforeEach
    void setUp() {
        new MockServerClient(
            mockServer.getHost(),
            mockServer.getServerPort()
        )
        .when(
            request().withMethod("GET").withPath("/api/data")
        )
        .respond(
            response()
                .withStatusCode(200)
                .withBody("{\"key\": \"value\"}")
        );
    }
}

8. Best Practices

8.1 Stub File Management

  • Version control stub mapping files with Git
  • Prepare different mapping sets for different environments
  • Use meaningful names for mapping files (e.g., get-user-by-id.json)

8.2 Creating Realistic Mock Data

// Generate realistic test data with Faker.js
const { faker } = require('@faker-js/faker')

function generateMockUsers(count) {
  return Array.from({ length: count }, (_, i) => ({
    id: i + 1,
    name: faker.person.fullName(),
    email: faker.internet.email(),
    phone: faker.phone.number(),
    address: faker.location.streetAddress(),
    createdAt: faker.date.past().toISOString(),
  }))
}

8.3 Testing Error Scenarios

Always prepare stubs for the following error situations:

  • 4xx errors: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests
  • 5xx errors: 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable
  • Network errors: Connection timeout, Connection reset, Empty response
  • Slow responses: Delays exceeding timeout thresholds

8.4 Combining Contract Testing and Mock Servers

┌─────────────────────────────────────────────────┐
│           Test Strategy Pyramid                  │
│                                                 │
│           ┌───────────────┐                     │
│           │  E2E Tests    │  <- Real environment │
│          ┌┴───────────────┴┐                    │
│          │ Contract Tests  │  <- Pact/SCC        │
│         ┌┴─────────────────┴┐                   │
│         │ Integration Tests │  <- Mock Server    │
│        ┌┴───────────────────┴┐                  │
│        │   Unit Tests        │  <- Mockito etc   │
│        └─────────────────────┘                  │
└─────────────────────────────────────────────────┘

8.5 Mock Server Selection Guidelines

  • Frontend development: json-server (quick start, auto CRUD support)
  • API-First development: Prism (auto mock based on OpenAPI spec)
  • Java/Spring integration testing: WireMock (Testcontainers integration, powerful matching)
  • Dynamic scenario testing: MockServer (runtime Expectation changes)
  • Traffic analysis/debugging: mitmproxy (real traffic interception)
  • Contract testing: Pact (Consumer-Driven) or Spring Cloud Contract (Provider-Driven)

9. Advanced Pattern: Service Virtualization

Beyond mocking servers, Service Virtualization is the concept of fully simulating the complex behavior of real services.

9.1 Simulating Workflows with WireMock + Stateful Scenarios

{
  "mappings": [
    {
      "scenarioName": "PaymentFlow",
      "requiredScenarioState": "Started",
      "newScenarioState": "PaymentPending",
      "request": { "method": "POST", "urlPath": "/api/payments" },
      "response": {
        "status": 201,
        "jsonBody": { "paymentId": "PAY-001", "status": "pending" }
      }
    },
    {
      "scenarioName": "PaymentFlow",
      "requiredScenarioState": "PaymentPending",
      "newScenarioState": "PaymentCompleted",
      "request": { "method": "POST", "urlPath": "/api/payments/PAY-001/confirm" },
      "response": {
        "status": 200,
        "jsonBody": { "paymentId": "PAY-001", "status": "completed" }
      }
    },
    {
      "scenarioName": "PaymentFlow",
      "requiredScenarioState": "PaymentCompleted",
      "request": { "method": "GET", "urlPath": "/api/payments/PAY-001" },
      "response": {
        "status": 200,
        "jsonBody": { "paymentId": "PAY-001", "status": "completed", "amount": 99.99 }
      }
    }
  ]
}

9.2 Leveraging the WireMock Admin API

# List all mappings
curl http://localhost:8080/__admin/mappings

# View request logs
curl http://localhost:8080/__admin/requests

# Check request count for specific pattern
curl -X POST http://localhost:8080/__admin/requests/count \
  -H "Content-Type: application/json" \
  -d '{"method": "GET", "url": "/api/users"}'

# Reset scenario states
curl -X POST http://localhost:8080/__admin/scenarios/reset

# Reset all mappings
curl -X POST http://localhost:8080/__admin/mappings/reset

10. Conclusion

Mocking servers are essential tools in modern software development. Choosing the right tool and using it properly can speed up development, ensure test reliability, and stabilize CI/CD pipelines.

Key takeaways:

  • Understand the difference between Stub vs Mock and use them appropriately
  • WireMock is the most versatile and powerful choice
  • json-server can be quickly applied to frontend development
  • Prism is optimal for API-First workflows
  • Combining Contract Testing with mock servers completes your microservices testing strategy
  • Use Testcontainers in CI/CD to automatically manage mock servers