icon-search
lauren-mancke-60627-unsplash

Using Spring REST Docs for testing and documenting APIs

Tomek Kopczyński 30.01.2017

Have you ever seen an API documentation that’s completely out of date? Or the one that seems to be updated but examples somehow don’t work? Or the one that’s automatically generated but you can’t find anything useful in the overflow of available methods? It definitely sounds familiar to me. Fortunately, there’s a tool which helps to produce a good, readable and up-to-date API documentation. Meet Spring REST Docs.

Introduction

We’ve first heard about Spring REST Docs on SpringIO 2016 conference in Barcelona. Since we really like automating as much as we can in delivery processes of projects, we started using it immediately after getting back to the office. Usage simplicity and the possibility of enforcing documentation accuracy convinced us that this was the right choice. Consequently, now we’re moving more and more our APIs integration tests to Spring REST Docs.

How does it work?

Spring REST Docs takes advantage of the following observation: why not combine running unit tests and creating documentation and have both executed in one step? Taking this even further, suppose a developer tries to document something which does not exist in the code (for example: a parameter to a service call). In this situation, the test should fail because the documentation would not be correct. Consequently, the same logic can be applied to response fields. As a result, generating documentation in a test-driven manner assures accuracy quite easily.

Furthermore, think about API documentation as a combination of a hand-written description and examples of requests and appropriate responses. The former should be obviously provided by the developer. Whereas the latter could (and should) be sniffed from an execution that actually took place. This is what you get by using Spring REST Docs.

At first, this test-documentation duality may seem odd but after some thought it starts making sense. It becomes clear that creating proper documentation for your API should be a part of the delivery process.

Technology mix in Spring REST Docs

Spring REST Docs uses a combination of technologies to achieve its results. First of all, you need to have a unit test engine in place. Apart from that you need tools for:

  • performing and validating service calls
  • generating documentation.

In our setup we’re using JUnit, Spring MockMvc (or REST Assured, depending on the requirements for testing) and Asciidoctor, respectively. This choice proved to be working well for us. Additionally, it’s quite easy to get it up and running since Spring REST Docs guide covers using these technologies quite extensively.

How to get it working?

In all examples below I will be using the setup based on the technology choice described above, with Spring MockMvc for making REST calls. Full source code for those examples is available in this GitHub repository.

Changes in pom.xml file

Spring REST Docs will be available for use after making some changes in pom.xml file.

 
<dependency>
   <groupId>org.springframework.restdocs</groupId>
   <artifactId>spring-restdocs-mockmvc</artifactId>
   <scope>test</scope>
</dependency>
<plugin>
   <groupId>org.asciidoctor</groupId>
   <artifactId>asciidoctor-maven-plugin</artifactId>  
   <version>1.5.3</version>
   <executions>     
     <execution>
            <id>generate-docs</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>process-asciidoc</goal>
            </goals>
            <configuration>       
                <backend>html</backend>
                <doctype>book</doctype>
                <outputDirectory>
                   ${project.build.outputDirectory}/static/docs
                </outputDirectory>
                <attributes>
                    <snippets>${project.build.directory}/generated-snippets</snippets>
                </attributes>
            </configuration>
        </execution>
    </executions>
</plugin>

Snippets above define the dependency for both Spring REST Docs and MockMvc. Additionally, there’s a plugin for Asciidoctor. Notice that it is executed in the prepare-package phase which means that it will run just before creating the final artifact. So, if we have a Spring Boot application, we can take advantage of its static content handling features (read about this here). To do that I instructed asciidoctor-maven-plugin to put its results into the ${project.build.outputDirectory}/static/docs directory.

Unit tests configuration

Each JUnit test needs to have two elements defined.


@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets");

First of all, we need to have a rule that defines the target directory where code snippets will be generated. It should match the directory written in the pom.xml configuration above.


import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;

private MockMvc mockMvc;

@Before
public void setUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
        .apply(documentationConfiguration(this.restDocumentation))
	.build();
}

Additionally, we need to create MockMvc instance with additional REST Docs configurer which applies some defaults for documenting calls to MockMvc.

Writing tests with Spring REST Docs

Let me guide you through some examples of writing tests with JUnit, MockMvc and Spring REST Docs. Imagine that we have a CMS system with the following endpoints:

  • retrieving a document by its ID: /document/{id}
  • creating a new document with a POST request: /document
  • status of the system: /status

Basic example


@Test
public void responseCodeTest() throws Exception {
    this.mockMvc.perform(
        get("/cms/status")
            .accept(MediaType.APPLICATION_JSON)
    )
    .andExpect(status().isOk())
    .andDo(
        document("status")
    );
}

This is a basic example of wiring a MockMvc test with Spring REST Docs. As you can see it calls the status endpoint and checks the status code – this is the standard integration test part. This snippet, however, does more than that. It also fires up the Spring REST Docs logic which will create all necessary snippets related to this call. All this is possible because we have put the .andDo(document("status")) line into the code. It serves as an entrypoint for Spring REST Docs into the MockMvc library.

Request and response snippets from this call will be written into the <output-directory> /status directory to be used by the Asciidoctor plugin. More about this in the section below.

Documenting request parameters

Core functionality of Spring REST Docs is documenting request parameters and response fields. This is the place where it enforces rules which guarantee the accuracy of the generated documentation. Simply put: if you try to document something non-existent or forget to document some part of request or response you will end up with a failing test. As a result you won’t be able to release a new version of your application without properly documented API. So, let’s look at request parameters at first.

@Test
public void retrieveDocumentTest() throws Exception {
    this.mockMvc.perform(get("/cms/document/{id}", 1L))
        .andExpect(status().isOk())
        .andDo(document("retrieveDocument",
            pathParameters(
                parameterWithName("id").description("Document's id")
            )
    ));
}

Here we are documenting an endpoint for retrieving a JSON representation of a document. It consumes one parameter: an id of the document. As you may have noticed, we’ve added the pathParameters method invocation which is responsible for retrieving all information about path parameters of the endpoint. Here we have provided the name and the description of the id parameter. This configuration will eventually create a snippet with a table with all request parameters and their descriptions to be included in the final documentation.

Documenting response fields

Similar functionality is available for response fields. Let’s look at an example for the same endpoint as before but now let’s focus on the response.

@Test
public void retrieveDocumentTest() throws Exception {
    this.mockMvc.perform(get("/cms/document/{id}", 1L))
        .andExpect(status().isOk())
        .andDo(document("retrieveDocument",
            responseFields(
                fieldWithPath("author").description("Document's author"),
                fieldWithPath("title").description("Document's title")
            )
        ));
}

By executing the responseFields method we’re telling Spring REST Docs to create a snippet with a table of response fields. It will look similar to the request parameters snippet. Additionally, it is worth noting that the argument for the fieldWithPath method is in fact a jsonpath String. This makes it quite flexible for navigating to the specific field no matter what the response structure looks like.

POST request

In this example we are making a call to a different endpoint than before. More specifically, we are sending a POST request with a JSON representation of a new document.The system is supposed to save it and return the newly created id as the result.

@Test
public void createDocumentTest() throws Exception {
    Document document = new Document("Jack Tester", "Testing REST APIs");
    this.mockMvc.perform(
        post("/cms/document")
            .content(objectMapper.writeValueAsString(document))
            .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isCreated())
        .andDo(document("createDocument",
            responseFields(
                fieldWithPath("id").description("Created document's id"))
        ));
}

This example doesn’t trigger any new documentation features (look at the document method). However, when dealing with POST requests it is worth noting that Spring REST Docs creates a snippet with a corresponding curl request by default. It is intended to be copy-pasted from the documentation into a terminal window to test the appropriate service by the user himself. In this case we will get the following snippet:


$ curl 'http://localhost:8080/cms/document' -i -X POST -H 'Content-Type: application/json' -d '{"author":"Jack Tester","title":"Testing REST APIs"}'

Request and response preprocessing

Spring REST Docs allow us to apply some changes to requests and responses before they are documented. This way we can for example mask sensitive headers or pretty print our responses. This is easily achievable by editing our setUp method:

private RestDocumentationResultHandler documentationHandler;

@Before
public void setUp() {
    this.documentationHandler = document("{method-name}",
        preprocessRequest(removeHeaders("Authorization")),
        preprocessResponse(prettyPrint()));

    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
        .apply(documentationConfiguration(this.restDocumentation))
        .alwaysDo(documentationHandler)
        .build();
}

As you can see we now have a RestDocumentationResultHandler instance which is responsible for executing the following actions:

  • Removing Authorization header from requests
  • Pretty printing JSON responses

Additionally, we’re putting this newly created object as an argument to the alwaysDo method call. This ensures that logic created in the documentationHandler object will be applied to all our tests.

Formatting documentation

As previously mentioned, we’re using Asciidoctor for formatting our documentation. Its syntax is very readable and is pretty obvious what it does just by looking at the code. Just to give you an example:


== Retrieve a document by ID

Request parameters:

include::{snippets}/retrieveDocument/path-parameters.adoc[]

curl request:

include::{snippets}/retrieveDocument/curl-request.adoc[]

HTTP response:

include::{snippets}/retrieveDocument/http-response.adoc[]

Response fields:

include::{snippets}/retrieveDocument/response-fields.adoc[]


In the snippet above we’re creating a section called Retrieve a document by ID. It consists of four snippets generated by Spring REST Docs while executing the service call. Those snippets are (in the order they appear in the example above):

  • path parameters
  • curl request
  • HTTP response
  • response fields

The include directive which is used for adding a particular snippet to the documentation uses the snippets variable. Its value is taken from the build configuration (to be exact: it is the snippets attribute from the plugin configuration in pom.xml file).

Generated documentation

Documentation generated with Spring REST Docs based on the example described above will look like this:

Spring REST Docs generated documentation

Quality documentation

As you can see from the example above, using Spring REST Docs allows you to describe services from a different angle than it can be often found. First of all, you should start by laying out a use case in your system. Then, you should follow up with the listing of all appropriate services. This feels more natural because people learn APIs by searching for particular usages that they are interested in. Doing this the other way around (by showing a list of available methods at the very start) makes it really difficult to find exactly what you are looking for. For example, How can I retrieve all ordered items for a particular user? (and similar). In our opinion, making sure that your documentation follows these rules will lead to creating very readable and user-friendly piece of reading.

Wrap-up

Summing up, remember that Spring REST Docs is not a silver bullet. It helps you maintain accuracy of your documentation and keeps your attention on the use cases from the user point of view. But nevertheless it is up to the developer to provide quality hand-written parts of the documentation. So, keep this in mind and you will be able to lift the quality of your API documentation to a whole new level.

comments: 0