Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Documenting APIs with Spring REST Docs

Documenting APIs with Spring REST Docs

Introduction to Spring REST Docs as a tool for a test-driven approach to writing and maintaining API documentation.

Tomasz Kopczynski

April 22, 2017
Tweet

More Decks by Tomasz Kopczynski

Other Decks in Programming

Transcript

  1. ABOUT ME ▸ Tomasz Kopczyński (@t_kopczynski) ▸ Software developer at

    Sparkbit (@_sparkbit_) ▸ Open source contributor (Docker, Apache Camel, Spring REST Docs)
  2. I ALWAYS VALIDATE MY DOCUMENTATION AFTER EVERY CHANGE IN THE

    CODEBASE, I JUST LIKE DOING THAT. nobody, never
  3. INACCURATE DOCUMENTATION CAN LEAD TO… ▸ in Open Source: feeling

    that your project is abandoned ▸ in User’s Manual: users feeling lost when they don’t know how to use your software ▸ in API docs: anger when executing documented actions but getting undocumented results ▸ in Project Management: waste of time when introducing a new developer to the project
  4. INTEGRATION TESTS TO THE RESCUE! ▸ API documentation ▸ Generate

    your request/response snippets while running your test suite ▸ Creating snippets from real calls has 100% accuracy guaranteed ▸ Rules: ▸ Fail the test when trying to document a non-existent parameter ▸ Fail the test when omitting a parameter in the documentation
  5. SPRING REST DOCS ▸ Snippets: ▸ Asciidoctor ▸ Markdown ▸

    Calling HTTP endpoints: ▸ Spring MockMvc ▸ REST Assured
  6. SPRING MOCK MVC ▸ Applications written with Spring MVC ▸

    No HTTP communication ▸ No need to start the application container ▸ Much faster than typical integration tests
  7. REST ASSURED ▸ Real HTTP communication underneath ▸ Endpoints can

    be written in any technology ▸ Well-suited for describing the communication with external systems
  8. EXAMPLE SYSTEM FOR CODE SAMPLES ▸ CMS system with the

    following REST endpoints: ▸ Create a document ▸ Retrieve a document ▸ Retrieve new documents since last request ▸ Healthcheck
  9. SPRING REST DOCS TEST WITH MOCKMVC @Test public void responseCodeTest()

    throws Exception { this.mockMvc.perform( get("/cms/healthcheck") .accept(MediaType.APPLICATION_JSON) ) .andExpect(status().isOk()) .andDo( document("healthcheck") ); }
  10. SPRING REST DOCS TEST WITH MOCKMVC @Test public void responseCodeTest()

    throws Exception { this.mockMvc.perform( get("/cms/healthcheck") .accept(MediaType.APPLICATION_JSON) ) .andExpect(status().isOk()) .andDo( document("healthcheck") ); } Spring REST Docs entrypoint
  11. POM.XML CONFIGURATION - DEPENDENCIES <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <scope>test</scope> </dependency> <dependency>

    <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-restassured</artifactId> <scope>test</scope> </dependency>
  12. POM.XML CONFIGURATION - ASCIIDOCTOR <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> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-asciidoctor</artifactId> <version>1.2.0.RC1</version> </dependency> </dependencies> </plugin>
  13. DEFAULT SNIPPETS ▸ curl request ▸ HTTPie request ▸ HTTP

    request ▸ HTTP response ▸ Request body (new in 1.2.0) ▸ Response body (new in 1.2.0)
  14. DOCUMENTING HTTP COMMUNICATION ▸ HTTP request [source,http,options="nowrap"] ---- POST /cms/document

    HTTP/1.1 Authorization: dXNlcjpwYXNzd29yZA== Content-Type: application/json Host: localhost:8080 Content-Length: 52 {"author":"Jack Tester","title":"Testing REST APIs"} —— ▸ HTTP response [source,http,options="nowrap"] ---- HTTP/1.1 201 Created X-Created-Id: 3 Content-Type: application/json;charset=UTF-8 Content-Length: 8 {"id":3} ----
  15. DOCUMENTING COMMAND-LINE TOOLS ‣ curl: [source,bash] ---- $ curl 'http://localhost:8080/cms/document'

    -i -X POST \ -H 'Authorization: dXNlcjpwYXNzd29yZA==' \ -H 'Content-Type: application/json' \ -d '{"author":"Jack Tester","title":"Testing REST APIs"}' ---- ‣ HTTPie: [source,bash] ---- $ echo '{"author":"Jack Tester","title":"Testing REST APIs"}' | http POST 'http://localhost:8080/cms/document' \ 'Authorization:dXNlcjpwYXNzd29yZA==' \ 'Content-Type:application/json' ----
  16. DOCUMENTING REQUEST PARAMS @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") ) )); } ./cms/document/{id} |=== |Parameter|Description |`id` |Document's id |===
  17. DOCUMENTING RESPONSE FIELDS @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") ) )); } |=== |Path|Type|Description |`author` |`String` |Document's author |`title` |`String` |Document's title |===
  18. FORMATTING DOCUMENTATION IN ASCIIDOCTOR = CMS API documentation :doctype: book

    :icons: font :toc: == 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[]
  19. HYPERMEDIA ▸ REST integration pattern ▸ Client holds only one

    URL for starting the communication ▸ Resources are fetched by following links available as part of the previous response ▸ Very loose coupling between server and client
  20. HYPERMEDIA { "author": "Harry Smith", "title": "Meeting report", "_links": {

    "self": { "href": "http://localhost:8080/cms/document/1" }, "all": { "href": "http://localhost:8080/cms/document" } } }
  21. RELAXED MODE ▸ Loosen standard validation rules ▸ Any undocumented

    element won’t cause a test failure ▸ Documented but not present element still fails a test ▸ Documentation correct but not complete ▸ Available for: ▸ request parameters (query and path) ▸ response fields ▸ links in Hypermedia ▸ parts in multipart transfer
  22. RELAXED MODE responseFields( subsectionWithPath("_links") .description("Links self"), fieldWithPath("author") .description("Document's author"), fieldWithPath("title")

    .description("Document's title") ) relaxedResponseFields( subsectionWithPath("_links") .description("Links self") )
  23. REUSING SNIPPETS private LinksSnippet pagingSnippets = links( halLinks(), linkWithRel("next") .optional()

    .description("The next page"), linkWithRel("prev") .optional() .description("The previous page”)); document("retrieveDocument", this.pagingSnippets.and( linkWithRel("all") .description("All available documents"), linkWithRel("self") .ignored() .optional()))
  24. COOKIES SUPPORT this.mockMvc.perform(get("/cms/document/new") .cookie(new Cookie("lastSeenDocumentId", "2"))) $ curl 'http://localhost:8080/cms/document/new' -i

    \ --cookie ‘lastSeenDocumentId=2' $ http GET 'http://localhost:8080/cms/document/new' \ 'Cookie:lastSeenDocumentId=2'
  25. SWAGGER @ApiResponses(value = { @ApiResponse(code = 401, message = "Unauthorized"),

    @ApiResponse(code = 403, message = "Forbidden"), @ApiResponse(code = 405, message = "Method not allowed") }) @ApiOperation(value = "Retrieve document metadata by a document key", response = DocumentMetadata.class) @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "DocumentMetadata business key", required = true, dataType = "string", paramType = "path") }) @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public DocumentMetadata findById(@PathVariable("id") String id) { return null; }
  26. GOOD REST DOCS DOCUMENTATION CHARACTERISTICS ▸ Focuses on use cases,

    not API methods ▸ Feels more like a document than a list of endpoints ▸ As much human-readable as possible ▸ Includes examples from real executions ▸ Is subject to test as well (to some extent)