Even better documentation with Spring Auto REST Docs

July 23, 2018

Context

In an earlier post we learned how to produce documentation for REST APIs from unit tests using the great Spring REST docs.

Still, some of us may think it's a bit boilerplate to have to manually add the document(...) directives to each of our tests in order to get a complete documentation. Wouldn't be wonderful to have those also generated without writing it for each test?

Fortunately, thanks to Spring Auto REST Docs, that is possible! Spring Auto REST Docs can generate the snippets for each and every test automatically. Moreover, it also consumes the Javadoc information provided in our domain objects and in our controllers to complete the API documentation. Seems nice, right? Let's take a look!

Note that Spring Auto REST Docs is an improved version of Spring REST Docs developed and maintained by Scalable Capital, not by the Spring team itself.

Hands on!

Let's take over from the project used in the other post Api Documentation with Spring REST Docs, where we had a Customers API implemented with Spring.

First, we need to configure the dependency and the plugins required to use this enhanced version of Spring REST Docs, by just following the getting started guide of Spring Auto REST Docs. Note that in this new version we have configured the project with JUnit 5 so we can show how Spring REST Docs works with it as well.

Once everything is properly configured, let's begin to add some Javadoc details to both our domain class Customer and to the method create in CustomersController.

In Customer:

/**
 * The customer object contains main details about a Customer
 */
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Customer {

    /**
     * Unique identifier of the customer
     */
    @Id
    private Long id;

    /**
     * First name of the customer
     */
    @NotNull
    private String firstName;

    /**
     * Last name of the customer
     */
    private String lastName;

    /**
     * Age in years of the customer
     */
    @NotNull
    private Integer age;

    /**
     * Gender of the customer
     */
    private Gender gender;

    /**
     * Phone number of the customer
     */
    private String phoneNumber;

}

In CustomersController:


// ...

     /**
     * Creates a new customer
     * @param customer the details of the new customer
     * @param salesRepId the identifier of the sales rep assigned to the customer
     * @return the customer details after creating
     */
    @PostMapping("/{salesRepId}")
    @ResponseStatus(HttpStatus.CREATED)
    public Customer create(@RequestBody @Valid Customer customer, @PathVariable Long salesRepId) {
        Assert.isNull(customer.getId(), "Id must be null to create a new customer");
        validate(salesRepId, customer);
        return this.customersRepository.save(customer);
    }
    
// ...

Note that we have used Lombok to clean up our domain object getting rid of getters, setters and constructors. For more information check their website.

OK, next step is to update our test configuration. We are going to create a separated test class for these test so we can compare between the two approaches. This is the setup we need to generate all the snippets automatically for each test:


//...
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mockMvc;

    @BeforeEach
    void setUp(RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders
                .webAppContextSetup(context)
                .alwaysDo(JacksonResultHandlers.prepareJackson(objectMapper))
                .apply(MockMvcRestDocumentation.documentationConfiguration(restDocumentation)
                        .uris()
                        .withScheme("http")
                        .withHost("demo.mimacom.com")
                        .withPort(443)
                        .and().snippets()
                        .withDefaults(CliDocumentation.curlRequest(),
                                HttpDocumentation.httpRequest(),
                                HttpDocumentation.httpResponse(),
                                AutoDocumentation.requestFields(),
                                AutoDocumentation.responseFields(),
                                AutoDocumentation.pathParameters(),
                                AutoDocumentation.requestParameters(),
                                AutoDocumentation.description(),
                                AutoDocumentation.methodAndPath(),
                                AutoDocumentation.section()))
                .alwaysDo(document("{class-name}/{method-name}",
                        preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
                .build();
    }
       
//...

As you can see, we are specifying the scheme, host and port to be used in the example requests and also the list of snippets to generate automatically for every test case.

Last, but not least, the test. We will just focus on testing the logic of the method, no need to add document(...) instructions:


//... setup

    @Test
    void createCustomer() throws Exception {

        Customer customer = new Customer();
        customer.setAge(23);
        customer.setFirstName("Liam");
        customer.setGender(Gender.MALE);
        customer.setPhoneNumber("1119992");

        when(this.customersRepository.save(any(Customer.class))).then((Answer<Customer>) invocationOnMock -> {
            if (invocationOnMock.getArguments().length > 0 && invocationOnMock.getArguments()[0] instanceof Customer) {
                Customer mockCustomer = (Customer) invocationOnMock.getArguments()[0];
                mockCustomer.setId(34L);
                return mockCustomer;
            }
            return null;
        });

        this.mockMvc.perform(RestDocumentationRequestBuilders.post("/customers/{salesRepId}", 330).content(this.objectMapper.writeValueAsString(customer))
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.firstName").value("Liam"))
                .andExpect(jsonPath("$.lastName").isEmpty())
                .andExpect(jsonPath("$.gender").value(Gender.MALE.name()))
                .andExpect(jsonPath("$.phoneNumber").value("1119992"))
                .andExpect(jsonPath("$.age").value(23))
                .andExpect(jsonPath("$.id").value(34));

    }
    
//... more tests

All set! We just need to run the test and see what is generated in the folder build/generated-snippets:

generated snippets

You can now verify there is one file for each of the snippets defined in the setup of the test, for each test case. Some of them may be empty, depending on the characteristics of each method. The files prefixed with auto are the ones created by Spring Auto REST Docs containing the descriptions specified in the Javadoc. For example, auto-description.adoc contains the Javadoc description defined for the controller method and auto-method-path.adoc, the path of the controller method. It also generates a auto-section.adoc that contains links for each of the generated snippets so you do not have to bother including all the snippets one by one.

So now you can modify your index.adoc file to include the selected snippets for each of the tests or just include the section to display all the snippets at once:

== Auto generated

include::{snippets}/customers-controller-auto-docs-test/create-customer/auto-section.adoc[]

Which will produce the following:

documentation-auto-generated-section

You can play around using individual snippets or complete sections depending on your needs. You can check here what the final documentation looks like.

As showed here, with Spring Auto REST Docs you can not only generate the snippets of your request and response payloads but also the description and details of the methods automatically! Just adding information to the Javadoc and a simple configuration to your tests.

This is a cleaner way to use Spring REST Docs because the unit tests don't contain anything beyond the test itself.

Don't you want to use it right away?

You can see the complete project in Github at: https://github.com/mimacom/spring-rest-docs.

About the author: Sandra Gómez

Full-stack engineer at mimacom, although recently has a tight relationship with microservices.