Creating a standalone stub server is important in order not to depend on an API that isn't yet available, isn't complete, or is expensive to access during the development phase. The front-end team can easily advance its functionality by simulating all the requests and responses that will be produced by the APIs later.

In this story, we'll explain how to build a standalone stub server for stubbing REST APIs using Spring Cloud Contract WireMock.

· Prerequisites · OverviewWhat is WireMock?Spring Cloud Contract WireMock · Spring Boot Stub Server App SetupProject SetupCreate Mock JSON FilesContainerize application · Testing · Conclusion · References

Prerequisites

This is the list of all the prerequisites for following this story:

  • Java 17
  • Starter Boot 3.0.6
  • Maven 3.6.3
  • Optionally, Docker installed
  • Optionally, Docker compose installed
  • Postman or Insomnia

Overview

What is WireMock?

WireMock is a popular open-source tool for API mock testing. WireMock is a library for stubbing and mocking web services. It helps to create stable test and development environments, isolates 3rd parties, and simulates APIs that don't exist yet.

Spring Cloud Contract WireMock

The Spring Cloud Contract WireMock modules let you use WireMock in a Spring Boot application. It simplifies some aspects of configuration and eliminates some common issues that occur when running Spring Boot and WireMock together.

Spring Boot Stub Server App Setup

Project Setup

We will start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: starter-web, actuator.

None

After that, we need to add the dependency related to the Spring Cloud Contract WireMock.

  <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-contract-wiremock -->
 <dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-contract-wiremock</artifactId>
   <version>4.0.4</version>
</dependency>

We need to add the AutoConfigureWireMock annotation to start a WireMock server in the context of the Spring application. This will bind the port, HTTPS port, and stub files and locations of the WireMock server when the Spring Boot application is started.

None

AutoConfigureWireMock annotation to the location of the parent directory (in other words, __files is a subdirectory). You can use a Spring resource notation to refer to file:…​ or classpath:…​ locations. Generic URLs are not supported. A list of values can be given.

In this case, I registered WireMock JSON stubs from the resources classpath mappings and __files directories.

None

To read the body content from a file, place the file under the __files directory. By default this is expected to be under src/test/resources when running from the JUnit rule. When running standalone it will be under the current directory in which the server was started.

Create Mock JSON Files

To create the stub via the JSON API, the mock JSON document can either be posted to http://<host>:<port>/__admin/mappings or placed in a file with a .json extension under the mappings directory. Actually, WireMock always loads mappings from src/test/resources/mappings as well as the custom locations in the stubs attribute.

Let's start by creating a JSON mock for our book API.

None
  • Auth Login
None

JSON mapping file:

{
  "request": {
    "method": "POST",
    "url": "/v1/login",
    "bodyPatterns": [
      { "matchesJsonPath": "$.login" },
      { "matchesJsonPath": "$.password" }
    ]
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "*"
    },
    "jsonBody": {
        "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJzeXN0ZW0iLCJ0aW1lem9uZSI6IlVUQyIsInJvbGVzIjpbIlJPTEVfTURfU0VUX0FETUlOIl0sImF2YXRhciI6bnVsbCwiYXV0aG9yaXRpZXMiOlsiTURfU0VUX0FETUlOIl0sImNsaWVudF9pZCI6Im9hdXRoX2NsaWVudF9pZCIsImF1ZCI6WyJyZXNvdXJjZV9pZCJdLCJzY29wZSI6WyJyb2xlX2FkbWluIiwicm9sZV91c2VyIl0sImRlZmF1bHRsb2NhbCI6ImZyIiwiaWQiOiI2MGFkMzMyZWY1MTg2N2I0NDhkZjE2MjIiLCJmdWxsbmFtZSI6InN5c3RlbSBhZG1pbiIsImV4cCI6MTYyMjc0MjA3Niwiam9iIjpudWxsLCJqdGkiOiI0NzFhMmEzZi0wY2UxLTRlNDMtOTNhNy0yZDhlODE3ZTk1ZTIiLCJlbWFpbCI6ImJvb3R0ZWNobm9sb2dpZXMuY2lAZ21haWwuY29tIiwiZGF0ZWZvcm1hdCI6IkREL01NL1lZWVkifQ.uyISDckvXXCk9Oqb8h-llOK6z05gmDzEESNZ45XBLLs",
        "token_type": "bearer",
        "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJzeXN0ZW0iLCJ0aW1lem9uZSI6IlVUQyIsInJvbGVzIjpbIlJPTEVfTURfU0VUX0FETUlOIl0sImF2YXRhciI6bnVsbCwiYXV0aG9yaXRpZXMiOlsiTURfU0VUX0FETUlOIl0sImNsaWVudF9pZCI6Im9hdXRoX2NsaWVudF9pZCIsImF1ZCI6WyJyZXNvdXJjZV9pZCJdLCJzY29wZSI6WyJyb2xlX2FkbWluIiwicm9sZV91c2VyIl0sImF0aSI6IjQ3MWEyYTNmLTBjZTEtNGU0My05M2E3LTJkOGU4MTdlOTVlMiIsImRlZmF1bHRsb2NhbCI6ImZyIiwiaWQiOiI2MGFkMzMyZWY1MTg2N2I0NDhkZjE2MjIiLCJmdWxsbmFtZSI6InN5c3RlbSBhZG1pbiIsImV4cCI6MTYyMjc2MDA3Niwiam9iIjpudWxsLCJqdGkiOiIyYmVlODZkMy1jZGFiLTQ0NmYtYmIwNi1iYTcxNjgwYTU5NTciLCJlbWFpbCI6ImJvb3R0ZWNobm9sb2dpZXMuY2lAZ21haWwuY29tIiwiZGF0ZWZvcm1hdCI6IkREL01NL1lZWVkifQ.HZsmMgPaBllRfx8lDOX8OBupnzn9tJ7-6wK6PjrWk5s",
        "expires_in": 3599,
        "scope": "role_admin role_user",
        "id": "60ad332ef51867b448df1622",
        "email": "bootlabs@gmail.com",
        "username": "admin",
        "roles": [
          "ROLE_MD_SET_ADMIN"
        ],
        "jti": "471a2a3f-0ce1-4e43-93a7-2d8e817e95e2"
    }
  }
}

The login response contains jwt user authentication access tokens.

  • Get books
None

JSON mapping file:

{
  "request": {
    "method": "GET",
    "url": "/v1/books"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "*"
    },
    "jsonBody": [
      {
        "id": 1,
        "description": "netus et malesuada",
        "isbn": "X4J 5H8",
        "page": 62,
        "price": 529.0,
        "title": "arcu. Vestibulum ut",
        "authorId": 9
      },
      {
        "id": 2,
        "description": "mollis non,",
        "isbn": "M3Q 4G1",
        "page": 15,
        "price": 668.0,
        "title": "Nullam ut",
        "authorId": 2
      },
      {
        "id": 3,
        "description": "Maecenas mi felis, adipiscing fringilla, porttitor",
        "isbn": "B5W 1Y8",
        "page": 16,
        "price": 708.0,
        "title": "et ipsum cursus",
        "authorId": 5
      },
      {
        "id": 4,
        "description": "eros turpis non enim. Mauris quis turpis",
        "isbn": "Q1O 7Y6",
        "page": 46,
        "price": 642.0,
        "title": "Nulla tincidunt,",
        "authorId": 4
      },
      {
        "id": 5,
        "description": "tellus non magna. Nam ligula elit, pretium",
        "isbn": "Q0V 7Q9",
        "page": 86,
        "price": 656.0,
        "title": "purus, in",
        "authorId": 1
      },
      {
        "id": 6,
        "description": "a, facilisis non, bibendum sed, est.",
        "isbn": "V6Q 8T2",
        "page": 57,
        "price": 299.0,
        "title": "sagittis",
        "authorId": 3
      },
      {
        "id": 7,
        "description": "suscipit nonummy. Fusce fermentum fermentum arcu. Vestibulum ante ipsum",
        "isbn": "Q2T 8C5",
        "page": 68,
        "price": 891.0,
        "title": "ligula. Donec",
        "authorId": 8
      },
      {
        "id": 8,
        "description": "arcu. Vivamus sit amet risus. Donec egestas.",
        "isbn": "R5E 3I4",
        "page": 14,
        "price": 455.0,
        "title": "vel",
        "authorId": 6
      },
      {
        "id": 9,
        "description": "pede, nonummy ut, molestie in, tempus",
        "isbn": "I0W 6N9",
        "page": 33,
        "price": 874.0,
        "title": "lorem semper",
        "authorId": 8
      },
      {
        "id": 10,
        "description": "sed consequat auctor, nunc nulla vulputate dui, nec",
        "isbn": "U4E 5V8",
        "page": 7,
        "price": 185.0,
        "title": "vel arcu.",
        "authorId": 4
      },
      {
        "id": 11,
        "description": "new book created",
        "isbn": "10254857964",
        "page": 10,
        "price": 20.0,
        "title": "new book",
        "authorId": 1
      },
      {
        "id": 12,
        "description": "new book created",
        "isbn": "10254857964",
        "page": 10,
        "price": 20.0,
        "title": "new book",
        "authorId": 1
      },
      {
        "id": 13,
        "description": "new book created",
        "isbn": "10254857964",
        "page": 10,
        "price": 20.0,
        "title": "new book",
        "authorId": 1
      },
      {
        "id": 14,
        "description": "new book created",
        "isbn": "10254857964",
        "page": 10,
        "price": 20.0,
        "title": "new book",
        "authorId": 1
      },
      {
        "id": 15,
        "description": "new book created",
        "isbn": "10254857964",
        "page": 10,
        "price": 20.0,
        "title": "new book",
        "authorId": 1
      },
      {
        "id": 16,
        "description": "new book created",
        "isbn": "10254857964",
        "page": 10,
        "price": 20.0,
        "title": "new book",
        "authorId": 1
      },
      {
        "id": 19,
        "description": "new book created",
        "isbn": "10254857964",
        "page": 10,
        "price": 20.0,
        "title": "new book",
        "authorId": 1
      }
    ]
  }
}
  • Create new books
None

JSON mapping file:

{
    "request": {
        "method": "POST",
        "url": "/v1/book",
        "bodyPatterns": [
            { "matchesJsonPath": "$.[?(@.id)]" },
            { "matchesJsonPath": "$.description" },
            { "matchesJsonPath": "$.isbn" },
            { "matchesJsonPath": "$.page" },
            { "matchesJsonPath": "$.price" },
            { "matchesJsonPath": "$.title" },
            { "matchesJsonPath": "$.authorId" }
        ]
    },
    "response": {
        "status": 201,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "*"
        },
        "jsonBody": {
            "id": 1,
            "description": "netus et malesuada",
            "isbn": "X4J 5H8",
            "page": 120,
            "price": 451.0,
            "title": "best book",
            "authorId": 9
        }
    }
}

URLs can be matched either by equality or by regular expression. For advanced use with Wiremock, we can use Regex matching on path and query to restrict URLs. Additionally, Wiremock provides the concept of scenarios to help simulate the different states of a REST API. This allows us to create tests where the API we use behaves differently depending on the state it is in.

All mapping JSON files are available in the project directory:

None

Containerize application

The next step is to containerize our API in order to deploy it.

  1. Dockerfile:
# creates a layer from the openjdk:17-alpine Docker image.
FROM openjdk:17-alpine

MAINTAINER boottechnologies.ci@gmail.com

# cd /app
WORKDIR /app

# Refer to Maven build -> finalName
ARG JAR_FILE=target/spring-stub-server-*.jar

# cp target/spring-stub-server-0.0.1-SNAPSHOT.jar /app/spring-stub-server.jar
COPY ${JAR_FILE} spring-stub-server.jar

# java -jar /app/spring-stub-server.jar
CMD ["java", "-jar", "-Xmx1024M", "/app/spring-stub-server.jar"]

# Make port 8080 available to the world outside this container
EXPOSE 8080

2. Docker compose

version: '3.8'

services:
  api:
    build: .
    ports:
      - '8080:8080'
    container_name: mockapi

Testing

Let's test our application after deployment.

POST Login:

None

POST book:

None

GET books

None

Conclusion

In this story, We have seen how to build a standalone stub server for stubbing REST APIs using Spring Cloud Contract WireMock.

The complete source code is available on GitHub.

If you enjoyed this story, please give it a few claps 👏 for support.

Thanks for reading!

References