A primer on stubbing APIs and testing services

Last updated Jul 26, 2025 Published Jul 17, 2025

The content here is under the Attribution 4.0 International (CC BY 4.0) license

Testing APIs and services is a crucial part of software development, especially in microservices architectures. Mockserver is a tool that allows developers to stub APIs and simulate service behavior, making it easier to test applications without relying on actual service implementations. In addition to that, it allows you to think about the decisions you want to make in your application, and how you want to test them, before you actually hit the real service.

I came across Mockserver in two different contexts: first, when I was working on a project that had integrations with multiple external APIs in a monolith application, and second, when I was joining a team that was in a microservices architecture, integrating with other teams. In both cases, Mockserver is a tool for creating stubs and verifying interactions with external services. However, using mockserver requires a setup beforehand and configuration of the stubs, which can be a bit of a learning curve. In this article, my goal here is to provide resources from real scenarios on Mockserver and labora on other tools that can be used to stub APIs, so you can get started with them and use them in your projects.

Why stubbing APIs?

Stubbing APIs is a common practice in software development, especially when dealing with external services or APIs that are not yet implemented or are unreliable. Stubbing allows developers to simulate the behavior of these APIs, enabling them to test their code without relying on the actual service. This can be particularly useful in scenarios where the external service is slow, unavailable, or has unpredictable behavior.

Mocking vs stubbing?

If you are wondering the differences between those two definitions, there is a dedicated place that I dive into that.

Stubbing APIs can also help in isolating tests, making them more reliable and easier to maintain. This practice aligns with the concept of solitary tests, as opposed to sociable tests, where a unit is tested in complete isolation from its real dependencies. By using stubs, developers can focus on testing their own code without worrying about the state of the external service. By using stubs, developers can focus on testing their own code without worrying about the state of the external service. This might lead to faster feedback loops.

The mock server model

Mockserver offers different approaches to use it, for example, you can run it as a standalone server, or as a library in your application. The standalone server is useful for testing applications that interact with external APIs, while the library is useful for testing applications that interact with other services in the same application. The standalone server can be run in a Docker container, which makes it easy to set up and use in different environments. The library can be used in Java, Kotlin, and other languages, and it provides a simple API for creating stubs and verifying interactions.

The core concept is the expectation. An expectation is a rule that says when a specific request comes in (the matcher), then perform a specific action (like returning a response). The official MockServer documentation provides a detailed explanation of how this works via a visual diagram. In addition to that, it depicts the different modes that Mockserver run.

A walked example for Rest API and docker

Let’s walk through a simple example of how to use Mockserver to stub a REST API. We’ll create a mock server that simulates a simple user service with a single endpoint that returns a list of users.

Set up Mockserver

You can run Mockserver as a Docker container. Here’s a simple command to start it:


docker run --name mockserver --rm -d -p 1080:1080 mockserver/mockserver

This command will start Mockserver on port 1080. You can access the Mockserver web interface at http://localhost:1080, the output should look like this:

2025-07-17 06:45:08 5.15.0 INFO using environment variables:

  [

  ]

 and system properties:

  [
        mockserver.propertyFile=/config/mockserver.properties
  ]

 and command line options:

  [
        serverPort=1080
  ]

2025-07-17 06:45:09 5.15.0 INFO logger level is INFO, change using:
 - 'ConfigurationProperties.logLevel(String level)' in Java code,
 - '-logLevel' command line argument,
 - 'mockserver.logLevel' JVM system property or,
 - 'mockserver.logLevel' property value in 'mockserver.properties'
2025-07-17 06:45:09 5.15.0 INFO 1080 started on port: 1080

At this point, you have a running Mockserver instance that you can use to create expectations and simulate API responses. However, we haven’t created any expectations yet, so if you try to access the /users endpoint, you’ll get a 404 Not Found response.

Create an expectation

Now, let’s create an expectation for our user service. We’ll use the Mockserver client to set up an expectation that when a GET request is made to /users, it will return a predefined list of users. To use it, we will modify the docker command to include the expectation json file based on the mockserver.properties file.

In order to achieve that, we will create a directory named: config-mockserver/ and store two configurtion files. The first file is the definition of our expectations for this server, which we will name expectations.json:

[
  {
    "httpRequest": {
      "method": "GET",
      "path": "/users"
    },
    "httpResponse": {
      "body": {
        "type": "JSON",
        "json": [
          {
            "id": 1,
            "name": "Alice"
          },
          {
            "id": 2,
            "name": "Bob"
          }
        ]
      }
    }
  }
]

Next we need to inform mockserver of our expectations file, this is done through the file mockserver.properties. Under the same folder, create it with the following content:


mockserver.initializationJsonPath=/config/expectations.json

Finally, we need to mount the config-mockserver/ directory to the container when we run it. This way, Mockserver will read the expectations file and set up the stub for us. The docker command to run Mockserver with the expectation file would look like this:


docker run -d --rm --name mockserver -p 1080:1080 -v $(pwd)/config-mockserver:/config/ mockserver/mockserver

Once running, the docker will read the configuration set and should output something like this:

2025-07-17 07:13:38 5.15.0 INFO loading JSON initialization file:

  /config/expectations.json

2025-07-17 07:13:39 5.15.0 INFO creating expectation:

  {
    "httpRequest" : {
      "method" : "GET",
      "path" : "/users"
    },
    "httpResponse" : {
      "body" : [ {
        "id" : 1,
        "name" : "Alice"
      }, {
        "id" : 2,
        "name" : "Bob"
      } ]
    },
    "id" : "8cbaa312-3d8d-32df-92ee-34517b357ee2",
    "priority" : 0,
    "timeToLive" : {
      "unlimited" : true
    },
    "times" : {
      "unlimited" : true
    }
  }

Now test the stub by making a GET request to the /users endpoint:


curl http://localhost:1080/users

You should receive a response like this:


[
  {"id": 1, "name": "Alice"},
  {"id": 2, "name": "Bob"}
]

This response confirms that our expectation is working correctly. The Mockserver has intercepted the request and returned the predefined list of users. The mockserver documentation provides the details on the properties available for the server.

docker run command

In this section, the command used relied on the volume to mount the config-mockserver/ directory to the container, in a directory that mockserver can read the configuration and the expectations file. However, there is variations to run Mockserver, such as using the -e flag to set environment variables or using the -v flag to mount a volume with the expectations file directly, for example:

docker run --name mockserver --rm -d -p 1080:1080 -e MOCKSERVER_INITIALIZATION_JSON_PATH="/config/expectations.json"  -v $(pwd)/config-mockserver/expectations.json:/config/expectations.json mockserver/mockserver

This approach is the same used in the rabbitmq project, where the expectations file is mounted directly to the container.

Building up on the example

You can extend this example by adding more expectations, such as handling POST requests to create a new user or PUT requests to update an existing user. Let’s say we want to add a POST request to create a new user. We can modify the expectations.json file to include the following expectation:

{
  "httpRequest": {
    "method": "POST",
    "path": "/users"
  },
  "httpResponse": {
    "statusCode": 201,
    "body": {
      "type": "JSON",
      "json": {
        "id": 3,
        "name": "Charlie"
      }
    }
  }
}

Now, when we make a POST request to /users, we should receive a response with the newly created user:


curl -X POST http://localhost:1080/users -d '{"name": "Charlie"}' -H "Content-Type: application/json"

You should receive a response like this:


{"id": 3, "name": "Charlie"}

Now, we have a more complete mock server that simulates a user service with both GET and POST endpoints. You can continue to add more expectations for other HTTP methods or endpoints as needed. However, keep in mind that, if you hit the GET endpoint after the POST, you will still get the same response as before, since we didn’t change the expectation for the GET request.

PUT and DELETE requests can be added in a similar way, allowing you to simulate a full REST API with Mockserver. The flexibility of Mockserver allows you to define complex behaviors and responses based on the requests made to the server, let’s add those two endpoints to our expectations file:

{
  "httpRequest": {
    "method": "PUT",
    "path": "/users/1"
  },
  "httpResponse": {
    "statusCode": 200,
    "body": {
      "type": "JSON",
      "json": {
        "id": 1,
        "name": "Alice Updated"
      }
    }
  }
},
{
  "httpRequest": {
    "method": "DELETE",
    "path": "/users/2"
  },
  "httpResponse": {
    "statusCode": 204
  }
}

Now, you can test the PUT request to update a user:

curl -X PUT http://localhost:1080/users/1 -d '{"name": "Alice Updated"}' -H "Content-Type: application/json"

You should receive a response like this:

{"id": 1, "name": "Alice Updated"}

And for the DELETE request to remove a user:


curl -X DELETE http://localhost:1080/users/2

You should receive a response with a 204 No Content status, indicating that the user was successfully deleted.


HTTP/1.1 204 No Content

By now, you have a fully functional mock server that simulates a user service with GET, POST, PUT, and DELETE endpoints. This setup allows you to test your application without relying on an actual user service.

The final expectations file
[
  {
    "httpRequest": {
      "method": "GET",
      "path": "/users"
    },
    "httpResponse": {
      "body": {
        "type": "JSON",
        "json": [
          {
            "id": 1,
            "name": "Alice"
          },
          {
            "id": 2,
            "name": "Bob"
          }
        ]
      }
    }
  },
  {
    "httpRequest": {
      "method": "POST",
      "path": "/users"
    },
    "httpResponse": {
      "statusCode": 201,
      "body": {
        "type": "JSON",
        "json": {
          "id": 3,
          "name": "Charlie"
        }
      }
    }
  },
  {
    "httpRequest": {
      "method": "PUT",
      "path": "/users/1"
    },
    "httpResponse": {
      "statusCode": 200,
      "body": {
        "type": "JSON",
        "json": {
          "id": 1,
          "name": "Alice Updated"
        }
      }
    }
  },
  {
    "httpRequest": {
      "method": "DELETE",
      "path": "/users/2"
    },
    "httpResponse": {
      "statusCode": 204
    }
  }
]

A walked example for GraphQL and docker

GraphQL is a query language for APIs that allows clients to request only the data they need. Mockserver can also be used to stub GraphQL APIs, allowing developers to simulate the behavior of a GraphQL server and test their applications without relying on an actual GraphQL implementation.

REST vs GraphQL

If you are wondering about the differences between REST and GraphQL, there is a dedicated place that I dive into that. Also, if you are looking for a reactjs + relay setup with GraphQL, you can check out the relay example.

The process is similar to the REST API example, but instead of defining expectations for HTTP requests, we define expectations for GraphQL queries and mutations. As opposed to different endpoint in REST, GraphQL uses a single endpoint and a single status code. This is the basis we will use to stub the GraphQL API with Mockserver.

Set up Mockserver for GraphQL

You can run Mockserver for GraphQL in the same way as the REST API example, using Docker. The only difference is that we will define expectations for GraphQL queries and mutations instead of HTTP requests. Let’s jump into the expectation file, that is the same as before but targeting a GraphQL endpoint.

{
  "httpRequest": {
    "method": "POST",
    "path": "/graphql",
    "body": {
      "type": "JSON",
      "json": {
        "operationName": "GetUserById",
        "variables": {
          "userId": "1"
        }
      },
      "matchType": "ONLY_MATCHING_FIELDS" 
    }
  },
  "httpResponse": {
    "statusCode": 200,
    "headers": {
      "Content-Type": ["application/json"]
    },
    "body": {
      "data": {
        "user": {
          "id": "1",
          "name": "Maria",
          "email": "maria@example.com"
        }
      }
    }
  }
}

Following the same pattern as before, now we will run the Mockserver with the expectation file mounted to the container, and test the GraphQL endpoint.

curl -X POST http://localhost:1080/graphql \
-H "Content-Type: application/json" \
-d '{
  "query": "query GetUserById($userId: ID!) { user(id: $userId) { id name email } }",
  "variables": { "userId": "1" },
  "operationName": "GetUserById"
}'

The response should be the following:

{
  "data" : {
    "user" : {
      "id" : "1",
      "name" : "Maria",
      "email" : "maria@example.com"
    }
  }
}

Let’s know go over a error scenario, for example, if we try to query a user that does not exist, we can define an expectation for that as well.

{
  "httpRequest": {
    "method": "POST",
    "path": "/graphql",
    "body": {
      "type": "JSON",
      "json": { "operationName": "GetUserById", "variables": { "userId": "999" } },
      "matchType": "ONLY_MATCHING_FIELDS"
    }
  },
  "httpResponse": {
    "statusCode": 200,
    "body": {
      "errors": [
        {
          "message": "User with ID 999 not found.",
          "locations": [ { "line": 2, "column": 3 } ],
          "path": ["user"]
        }
      ]
    }
  }
}

Executing the same curl command as before, but with a user ID that does not exist, should return an error response:

curl -X POST http://localhost:1080/graphql \
-H "Content-Type: application/json" \
-d '{
  "query": "query GetUserById($userId: ID!) { user(id: $userId) { id name email } }",
  "variables": { "userId": "999" },
  "operationName": "GetUserById"
}'

Should return the following error response:

{
  "errors": [
    {
      "message": "User with ID 999 not found.",
      "locations": [ { "line": 2, "column": 3 } ],
      "path": ["user"]
    }
  ]
}

This response confirms that our expectation for the error scenario is working correctly. The Mockserver has intercepted the request and returned the predefined error response for the non-existing user. Note that the stubbed response for the error uses now the id 999, which is not the same as the one we used for the successful response. This is intentional to avoid creating conflict with the previous expectation.

Stubbing and TDD outside-in

Stubbing APIs is a common practice in software development, especially when dealing with external services or APIs that are not yet implemented or are unreliable. Stubbing allows developers to simulate the behavior of these APIs, enabling them to test their code without relying on the actual service. This can be particularly useful in scenarios where the external service is slow, unavailable, or has unpredictable behavior. Stubbing APIs can also help in isolating tests, making them more reliable and easier to maintain.

This becomes particularly useful in outside-in Test-Driven Development scenarios, where you want to write tests before the actual implementation is done, in addition to that, simulating edge cases and error handling scenarios that might be difficult to reproduce with the real service. By using stubs, developers can focus on testing their own code without worrying about the state of the external service.

The outside-in setup

It’s been a few years since I shared the template for outside-in TDD for frontend applications, and I still use it regularly.

Thinking about that, in outside-in TDD, this kind of utility becomes a basis for software design, allowing you to define the behavior of your application.

Alternatives to Mockserver

While Mockserver is a powerful tool for stubbing APIs, there are other alternatives available, out there, such as WireMock, MSW, and others. Each tool has its own strengths and weaknesses, and the choice of which one to use depends on the specific requirements of your project. In this section we will focus on other that are used to achieve the same goal, but with different approaches.

Mockserver

Mockserver as you might have seen so far, is useful when you need to simulate multiple endpoints and behaviors. It, stubs the envire server.

MSW

MSW is a Request Interceptor. It uses a Service Worker in the browser to intercept outgoing requests at the network level without them ever leaving the browser. In Node.js, it patches the native http/https modules to intercept requests. Your application code is completely unaware that its requests are being mocked.

Side project with something similar to MSW

In 2021, I was exploring the idea of using a service worker to intercept requests in a side project that in the end I ended up using fetch-mock instead. The idea was to create a simple way to mock API requests in the browser without having to change the application code.

MSW might be a first choice for frontend applications, especially when you want to mock API requests in the browser without having to change the application code. It allows you to define request handlers that intercept outgoing requests and return mock responses, making it easy to test your application without relying on actual API implementations.

WireMock

WireMock is a flexible tool for stubbing and mocking web services. It can be used to stub HTTP APIs, simulate service behavior, and verify interactions with external services. WireMock can be run as a standalone server or embedded in your application. It provides a rich set of features for defining expectations, simulating responses, and verifying requests. WireMock is particularly useful for testing applications that interact with external APIs or services.

Which one to choose?

The choice between these tools depends on a set of criteria, for example: what you are testing, where it is running, the experience of the developer picking up the tool, and other factors. However, making a simplistic guideline might help to get started with one or the other.

  • Choose MockServer or WireMock when you are testing a non-JavaScript client (like a Java backend service) or when you need a shared, language-agnostic mock server for integration and end-to-end tests. They excel at black-box testing where the mock lives in its own process.
  • Choose MSW when you are developing or testing a frontend JavaScript/TypeScript application. Its seamless interception model provides an unparalleled developer experience for frontend development, component testing, and Storybook.

As a final node, hese tools are more complementary than competitive. It’s common for a frontend team to use MSW while a backend team uses MockServer or WireMock within the same project. Another approach is to look at the type of testing that the team is aimg to do. Taking into account the shapes and type of tests that the team agreed upon might make things easier.

Feature MockServer MSW (Mock Service Worker) WireMock Price
Core Philosophy Network Proxy / Real Server Request Interceptor Network Proxy / Real Server Free
Environment Runs as a separate process (JVM/Docker). Language-agnostic. Runs inside your app’s process (Browser or Node.js). JS/TS only. Runs as a separate process (JVM/Docker) or can be embedded. Free
Setup Start Docker container/JAR. Configure via REST API or JSON/Java initializer. Install npm package. Write mocks in JS/TS. Integrate with app entry point. Start Docker container/JAR. Configure via REST API or JSON/Java DSL. Free
Network Level Application code must be configured to point to the mock’s URL (localhost:1080). Application code requests the real production URL. MSW intercepts the call. Application code must be configured to point to the mock’s URL (localhost:8080). Free

Resources

You also might like