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.

Avatar for Tomasz Kopczynski

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)