ePages-de / restdocs-wiremock

Spring REST Docs WireMock Integration
Apache License 2.0
75 stars 27 forks source link

Spring REST Docs WireMock Integration

[oss lifecycle]() Coverage

This is a plugin for auto-generating WireMock stubs as part of documenting your REST API with Spring REST Docs.

The basic idea is to use the requests and responses from the integration tests as stubs for testing your client's API contract. The mock templates can be packaged as jar files and be published into your company's artifact repository for this purpose.

Details and background information can be read on our ePages Developer Blog.

Contents

This repository consists of two libraries:

Versions

There are multiple major versions of the libraries in different branches. The following table provides an overview which "restdocs-wiremock" release branch is to be used for microservices on specific Spring Boot versions.

restdocs-wiremock version Spring Boot version Java version
1.x.x 3.3.x 21
0.x.x 2.7.x 17

How to include restdocs-wiremock into your server project

Dependencies

The project is published on Maven Central, so firstly, you need to add mavenCentral as package repository for your project.

Then, add restdocs-wiremock as a dependency in test scope. As restdocs-wiremock only depends on spring-restdocs-core, you also need to add either spring-restdocs-mockmvc or spring-restdocs-restassured, depending on your test scenario. It is recommended to use the spring-boot gradle plugin to enable dependency management from the Spring IO Platform.

In gradle it would look like this:

dependencies {
  testCompile('com.epages:restdocs-wiremock:1.0.0')
  testCompile('org.springframework.restdocs:spring-restdocs-mockmvc')
}

When using maven:

<dependency>
    <groupId>com.epages</groupId>
    <artifactId>restdocs-wiremock</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-mockmvc</artifactId>
    <scope>test</scope>
</dependency>

Producing snippets

During REST Docs run, snippets like the one below are generated and put into a dedicated jar file, which you can publish into your artifact repository.

Integration into your test code is as simple as adding wiremockJson() from com.epages.restdocs.WireMockDocumentation to the document() calls for Spring REST Docs. For example:

@RunWith(SpringJUnit4ClassRunner.class)
//...
class ApiDocumentation {
    // ... the usual test setup.
    void testGetSingleNote() {
        this.mockMvc.perform(get("/notes/1").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andDo(document("get-note",
          wiremockJson(),
          responseFields( ... )
        ));
    }
}

The snippet below is the resulting snippet of a 200 OK response to /notes/1, with the response body as provided by the integration test.

{
  "request" : {
    "method" : "GET",
    "urlPath" : "/notes/1"
  },
  "response" : {
    "status" : 200,
    "headers" : {
      "Content-Type" : [ "application/hal+json" ],
      "Content-Length" : [ "344" ]
    },
    "body" : "{\n  \"title\" : \"REST maturity model\",\n  \"body\" : \"http://martinfowler.com/articles/richardsonMaturityModel.html\",\n  \"_links\" : {\n    \"self\" : {\n      \"href\" : \"http://localhost:8080/notes/1\"\n    },\n    \"note\" : {\n      \"href\" : \"http://localhost:8080/notes/1\"\n    },\n    \"tags\" : {\n      \"href\" : \"http://localhost:8080/notes/1/tags\"\n    }\n  }\n}"
  }
}

More flexible request matching using urlPathPattern

The above snippet has a shortcoming. WireMock will only match a request if the url matches the complete path, including the id. This is really inflexible. We can do better by using RestDocumentationRequestBuilders to construct the request using a url template, instead of MockMvcRequestBuilders.

@RunWith(SpringJUnit4ClassRunner.class)
//...
class ApiDocumentation {
    // ... the usual test setup.
    void testGetSingleNote() {
        this.mockMvc.perform(RestDocumentationRequestBuilders.get("/notes/{id}", 1)
          .accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andDo(document("get-note",
          wiremockJson(),
          responseFields( ... )
        ));
    }
}

This generates a snippet that uses urlPathPattern instead if urlPath. So WireMock would match a request with any id value.

{
  "request" : {
    "method" : "GET",
    "urlPathPattern" : "/notes/[^/]+"
  },
  "response" : {
    //...
  }
}

Response Templating

WireMock stubs with static responses are too inflexible in many use cases. That is why WireMock offers Response Templating. Response Templating allows us to make our responses more dynamic by using expressions instead of static values.

restdocs-wiremock allows us to use response template expressions:

this.mockMvc.perform(get("/notes/{id}", id))
  .andDo(document("get-note",
    wiremockJson(idFieldReplacedWithPathParameterValue()),
    //..
));

idFieldReplacedWithPathParameterValue() is a convenience method offered by WireMockDocumentation for the common use case to replace the id field in a response with the value of the second path segment.

The following variant would give the same result:

wiremockJson(templatedResponseField("id").replacedWithWireMockTemplateExpression("request.requestLine.pathSegments.[1]"));

We can also address the path parameter by the URI template name.:

wiremockJson(templatedResponseField("id").replacedWithUriTemplateVariableValue("id");)

The resulting stub contains the expression and WireMock will replace the id field with the value of the path segment when serving a response based on this stub.

{
  "request" : {
    "method" : "GET",
    "urlPattern" : "/notes/[^/]+"
  },
  "response" : {
    "transformers" : [ "response-template" ],
    "body" : "{\"id\":\"{{request.requestLine.pathSegments.[1]}}\",\"title\":\"REST maturity model\"}",
    "status" : 200
  }
}

WireMock also offers expressions to generate random values.

wiremockJson(templatedResponseField("id").replacedWithWireMockTemplateExpression("randomValue length=33 type='ALPHANUMERIC'")

Also replacing date values often comes in handy:

wiremockJson(templatedResponseField("date").replacedWithWireMockTemplateExpression("now")

The WireMock stubs jar

On the server side you need to collect the WireMock stubs and publish them into an artifact repository. In gradle this can be achieved by a custom jar task.

task wiremockJar(type: Jar) {
    description = 'Generates the jar file containing the wiremock stubs for your REST API.'
    group = 'Build'
    classifier = 'wiremock'
    dependsOn project.tasks.test
    from (snippetsDir) {
        include '**/wiremock-stub.json'
        into "wiremock/${project.name}/mappings"
    }
}

On the client side, add a dependency to the test-runtime to the jar containing the WireMock stubs. After that, the JSON files can be accessed as classpath resources.

testRuntime (group:'com.epages', name:'restdocs-server', version:'1.0.0', classifier:'wiremock', ext:'jar')

How to use WireMock in your client tests

Integrating a WireMock server can easily be achieved by including our wiremock-spring-boot-starter into your project. It adds a wireMockServer bean, which you can auto-wire in your test code. By default, we start WireMock on a dynamic port, and set a wiremock.port property to the port WireMock is running on. This property can be used to point your clients to the location of the WireMock server.

Services based on spring-cloud-netflix, i.e. using feign and ribbon, are auto-configured for you.

Dependencies

To add a dependency via gradle, extend your build.gradle with the following line:

testCompile('com.epages:wiremock-spring-boot-starter:1.0.0')

When using maven, add the following dependency in test scope.

<dependency>
    <groupId>com.epages</groupId>
    <artifactId>wiremock-spring-boot-starter</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>

Important note for Spring Cloud users: The BOM produced by Spring Cloud includes a transitive resolution of WireMock version 1.55, (via spring-cloud-aws -> aws-java-sdk-core -> wiremock (test)). As versions from BOM always override transitive versions coming in through maven dependencies, you need to add an explicit dependency on WireMock 2.x to your project, like shown in the following gradle example:

  testCompile('com.github.tomakehurst:wiremock:2.10.1')

Configuring your test to use the WireMock stubs

Here is an excerpt of the sample test from a restdocs client project to illustrate the usage.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { ClientApplication.class })
@ActiveProfiles("test") // (1)
@WireMockTest(stubPath = "wiremock/restdocs-server") // (2)
public class NoteServiceTest {

    @Autowired
    private WireMockServer wireMockServer; // (3)

    ....
}
  1. Configure your test to point to your WireMock server. In our example we are using a Spring Expression inside application-test.properties to point our noteservice to WireMock: noteservice.baseUri=http://localhost:${wiremock.port}/
  2. The @WireMockTest annotation enables the wireMockServer bean, which can be accessed from your test's application context. By default, it starts a WireMockServer on a dynamic port, but you could also set it to a fixed port. The stubPath property can be used to point to a classpath resource folder that holds your json stubs.
  3. If you want, you can auto-wire the WireMockServer instance, and re-configure it, just as described in the official WireMock documentation.

It is possible to read-in a subset of mappings for each test, by repeating the @WireMockTest annotation on the test method. The stubPath is concatenated from the values given on the test class and the test method, just as a @RequestMapping annotation in Spring would. In the example given below, the resulting stubPath provided to WireMock is composed as wiremock/myservice/specific-mappings.

@WireMockTest(stubPath = "wiremock/myservice")
public class MyTest {
    @Test
    @WireMockTest(stubPath = "specific-mappings")
    public void testDifferentMappings() {
     ....
    }

Building from source

This project uses JDK 21, with source compatibility for Java 21. The JDK can be used via SDKMAN!.

Please execute at least step 1 + 2 if before importing restdocs-wiremock into your IDE.

(1) Publish the current restdocs-wiremock library code into your local maven repository.

./gradlew restdocs-wiremock:build restdocs-wiremock:publishToMavenLocal

(2) Run the server tests, which uses the WireMock integration into Spring REST Docs. As a result, there is a restdocs-server-wiremock jar file in your local maven repository. Mind that this jar only contains a set of json files without explicit dependency on WireMock itself.

./gradlew restdocs-server:build restdocs-server:publishToMavenLocal

(3) Run the client tests, that expect a specific API from the server. By mocking a server via WireMock the client can be tested in isolation, but would notice a breaking change.

./gradlew restdocs-client:build

Publishing

Given that the master branch on the upstream repository is in the state from which you want to create a release, execute the following steps:

(1) Create release via the GitHub UI

Use the intended version number as "Tag version".

This will automatically trigger a GitHub Action build which publishes the JAR files for this release to Sonatype.

(2) Login to Sonatype

Login to Sonatype and navigate to the staging repositories.

(3) Close the staging repository

Select the generated staging repository and close it. Check that there are no errors afterwards (e.g. missing signatures or Javadoc JARs).

(4) Release the repository

Select the generated staging repository and publish it. After few minutes, the release should be available in the "Public Repositories" of ePages.

(5) Update documentation

Create a new commit which replaces the previous version number with the new version number in this README file.

Other resources