Spring REST Docs is a project to document a RESTful API using tests. The tests are used to invoke real REST calls on the application and to generate Asciidoctor markup snippets. We can use the generated snippets in an Asciidoctor document with documentation about our API. We can use Spring REST Docs to document a REST API we create using Micronaut.
First we must change our build file and include the Asciidoctor plugin and add dependencies to Spring REST Docs. The following example Gradle build file adds the Gradle Asciidoctor plugin, Spring REST Docs dependencies and configures the test
and asciidoctor
tasks. Spring REST Docs supports three different web clients to invoke the REST API of our application: Spring MockMVC, Spring Webflux WebTestClient and REST Assured. We use REST Assured 3, because it has little dependencies on other frameworks (like Spring).
// File: build.gradle ... plugins { id "org.asciidoctor.convert" version "1.5.8.1" } ... ext { snippetsDir = file('build/generated-snippets') springRestDocsVersion = '2.0.2.RELEASE' } dependencies { asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:$springRestDocsVersion" testCompile "org.springframework.restdocs:spring-restdocs-restassured:$springRestDocsVersion" } test { outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir dependsOn test }
Let's add a controller to our application that has two methods to return one or more Conference
objects. We want to document both REST API resource methods. First we look at the Conference
class that is used:
// File: src/main/java/mrhaki/micronaut/Conference.java package mrhaki.micronaut; public class Conference { private final String name; private final String location; public Conference(final String name, final String location) { this.name = name; this.location = location; } public String getName() { return name; } public String getLocation() { return location; } }
Next we write the following controller to implement /conference
to return multiple conferences and /conference/{name}
to return a specific conference. The controller is dependent on the class ConferenceService
that contains the real logic to get the data, but the implementation is not important for our example to document the controller:
// File: src/main/java/mrhaki/micronaut/ConferenceController.java package mrhaki.micronaut; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Controller("/conference") public class ConferenceController { private final ConferenceService conferenceService; public ConferenceController(final ConferenceService conferenceService) { this.conferenceService = conferenceService; } @Get("/") public Flux<Conference> all() { return conferenceService.all(); } @Get("/{name}") public Mono<Conference> findByName(final String name) { return conferenceService.findByName(name); } }
Now it is time to write our test that will invoke our controller and generate Asciidoctor markup snippets. We use Spock for writing the test in our example:
// File: src/test/groovy/mrhaki/micronaut/ConferenceApiSpec.groovy package mrhaki.micronaut import io.micronaut.context.ApplicationContext import io.micronaut.http.HttpStatus import io.micronaut.runtime.server.EmbeddedServer import io.restassured.builder.RequestSpecBuilder import io.restassured.specification.RequestSpecification import org.junit.Rule import org.springframework.restdocs.JUnitRestDocumentation import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification import static io.restassured.RestAssured.given import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration class ConferenceApiSpec extends Specification { @Shared @AutoCleanup private EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) @Rule private JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() private RequestSpecification spec void setup() { // Create a REST Assured request specification // with some defaults. All URI's // will not have localhost as server name, // but api.example.com and the port is removed. // All JSON responses are prettyfied. this.spec = new RequestSpecBuilder() .addFilter( documentationConfiguration(restDocumentation) .operationPreprocessors() .withRequestDefaults( modifyUris().host('api.example.com') .removePort()) .withResponseDefaults(prettyPrint())) .build() } void "get all conferences"() { given: final request = given(this.spec) // The server port is set and the value is // used from embeddedServer. .port(embeddedServer.URL.port) .accept("application/json") .filter( document( "all", responseFields( fieldWithPath("[].name").description("Name of conference."), fieldWithPath("[].location").description("Location of conference.") ))) when: final response = request.get("/conference") then: response.statusCode() == HttpStatus.OK.code } void "get conference with given name"() { given: final request = given(this.spec) .port(embeddedServer.URL.port) .accept("application/json") .filter( document( "getByName", responseFields( fieldWithPath("name").description("Name of conference."), fieldWithPath("location").description("Location of conference.") ))) when: final response = request.get("/conference/Gr8Conf EU") then: response.statusCode() == HttpStatus.OK.code } }
Finally we create a Asciidoctor document to describe our API and use the generated Asciidoctor markup snippets from Spring REST Docs in our document. We rely in our example document on the operation
macro that is part of Spring REST Docs to include some generated snippets:
// File: src/docs/asciidoc/api.adoc = Conference API == Get all conferences operation::all[snippets="curl-request,httpie-request,response-body,response-fields"] == Get conference using name operation::getByName[snippets="curl-request,httpie-request,response-body,response-fields"]
We run the Gradle asciidoctor
task to create the documentation. When we open the generated HTML we see the following result:
Written with Micronaut 1.0.0.M4 and Spring REST Docs 2.0.2.RELEASE.