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

Test Driven Docs APIConf DE 2017

1f28a0c1988421be3268026bd6bb6f49?s=47 jlstrater
September 20, 2017

Test Driven Docs APIConf DE 2017

As presented at API Conference DE 2017 on September 20, 2017
------------

Documentation generated from source code is very popular. Solutions such as Swagger are available for many different languages and frameworks. However, limitations of annotation based tools are becoming apparent. An overwhelming number of documentation annotations make for great docs but muddy the source code. Then, something changes and the docs are out of date again.

That is where test-driven approaches come in. Test-driven documentation solutions, such as Spring REST Docs, generate example snippets for requests and responses from tests ensuring both code coverage and accurate documentation. It can even fail the build when documentation becomes out of date. This session will walk through how to implement test-driven documentation solutions. Attendees should have a basic understanding of a markdown like documentation tool such as AsciiDoc and how to construct RESTful APIs in a JVM ecosystem technology such as Spring Boot. Examples will be in Spring Boot and Groovy, but the concepts are applicable to other ecosystems too.

1f28a0c1988421be3268026bd6bb6f49?s=128

jlstrater

September 20, 2017
Tweet

More Decks by jlstrater

Other Decks in Technology

Transcript

  1. A Test-Driven Approach to Documenting RESTful APIs Jenn Strater @codeJENNerator

  2. @codeJENNerator Note For Those Viewing Slides Online • Bulleted text

    like this indicates the key points mentioned on a previous slide. They may not have been included in the official presentation. • If this view does not support links, the links will work in the pdf. In speakerdeck, you can click the ‘download pdf’ button at the right.
  3. @codeJENNerator • Background of API Documentation • Approaches • Considerations

    • Test-Driven Documentation • Spring REST Docs • Examples Outline
  4. @codeJENNerator https://speakerdeck.com/jlstrater/test-driven-docs-apiconf-de-2017 https://github.com/jlstrater/groovy-spring-boot-restdocs-example https://github.com/ratpack/example-books https://github.com/jlstrater/spring-restdocs-public-api-example Follow Along

  5. @codeJENNerator

  6. @codeJENNerator About Me - Senior Engineer at Zenjob as of

    June 2017 - Spent the last year in Copenhagen working on OSS and taking classes through a Fulbright Grant. - Prior to the Fulbright Grant, I was a senior consultant at Object Partners, Inc. in Minneapolis, MN, USA. My work since then is the subject of this talk. - Co-founder of Gr8Ladies and talk about women in the Groovy Community all over the world - Passionate about bring new people into the Groovy community through free introductory workshops called Gr8Workshops.
  7. @codeJENNerator Background

  8. @codeJENNerator Background Creating RESTful APIs • Spring Boot • Grails

    • Ratpack • Other MVC-like frameworks?
  9. @codeJENNerator Background Creating RESTful APIs • Spring Boot • Grails

    • Ratpack • Other MVC-like frameworks? API Documentation • Wiki Pages, Word Docs, Confluence, etc • Swagger/RAML • Asciidoctor
  10. @codeJENNerator img src: https://flic.kr/p/rehEf5

  11. @codeJENNerator I hate writing documentation!* img src: https://flic.kr/p/rehEf5

  12. @codeJENNerator Example Case Study

  13. @codeJENNerator Factors in Choosing a Documentation Solution

  14. @codeJENNerator REST Maturity Model img src: http://martinfowler.com/articles/richardsonMaturityModel.html

  15. @codeJENNerator REST Maturity Model img src: http://martinfowler.com/articles/richardsonMaturityModel.html

  16. @codeJENNerator • I’m not really a fan of the right

    vs wrong REST debate, but I like this categorization of APIs. • Most of our APIS were level one or two, but we wanted to have the flexibility to use hypermedia • Spring REST docs includes support for level 3 / hypermedia • Swagger 2.0 did not support hypermedia. Swagger 3.0 (released end of July 2017) now does
  17. @codeJENNerator

  18. @codeJENNerator As architecture evolves, many companies move from a central

    monolith to micro services or maybe even gateways and multi-tiered architectures. • For documentation, it was important to have: • a consistent look and feel • a way to show how the services can work together
  19. @codeJENNerator Central Information Security Http Verbs Error Handling Http Status

  20. @codeJENNerator Central Information • For example, security tokens, patterns for

    error messages, http verbs/status codes, etc • This information needs to be written out and defined once; not on every endpoint.
  21. @codeJENNerator Design

  22. @codeJENNerator Design VS

  23. @codeJENNerator • On the left, is a swagger ui example

    • On right, is an example from Spring Rest Docs using Asciidoc. Notice the very different way of organizing information. URIs are difficult to read whereas resource document design organizes information by topic and includes urls in the examples only.
  24. @codeJENNerator

  25. @codeJENNerator Definitions Swagger (OpenAPI) RAML

  26. @codeJENNerator Definitions Swagger (OpenAPI) RAML Documentation AsciiDoc Markdown Wikis

  27. @codeJENNerator Definitions Swagger (OpenAPI) RAML Documentation AsciiDoc Markdown Wikis Swagger

    UI Swagger2Markup
  28. @codeJENNerator Definitions Swagger (OpenAPI) RAML Testing MockMVC RestAssured Documentation AsciiDoc

    Markdown Wikis Swagger UI Swagger2Markup
  29. @codeJENNerator Definitions Swagger (OpenAPI) RAML Testing MockMVC RestAssured Documentation AsciiDoc

    Markdown Wikis Swagger UI Swagger2Markup AssertJ- Swagger Contract-First
  30. @codeJENNerator Definitions Swagger (OpenAPI) RAML Testing MockMVC RestAssured Documentation AsciiDoc

    Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First
  31. @codeJENNerator Definitions

  32. @codeJENNerator Swagger is: — a lot of things • At

    the core, it is a way to standardize and define HTTP APIs over RPC. • It is very popular because of the many plugins built on top of it for things such as generating client libraries, generating docs, and much more.
  33. @codeJENNerator Swagger Approaches for the JVM

  34. @codeJENNerator Automation img src: https://flic.kr/p/eduUfU

  35. @codeJENNerator img src: https://www.flickr.com/photos/ 24874528@N04/17125924230 SpringFox

  36. @codeJENNerator SpringFox: • Generates a Swagger Specification from source •

    Is very easy to setup (in simple cases)
  37. @codeJENNerator Installation • io.springfox:springfox-swagger2 • Some simple config in application

    main
  38. @codeJENNerator Custom Swagger Specification

  39. @codeJENNerator {
 "swagger": "2.0",
 "info": {
 "version": "1",
 "title": "My

    Service",
 "contact": {
 "name": "Company Name"
 },
 "license": {}
 },
 "host": "example.com",
 "basepath": "/docs",
 "tags": [
 {
 "name": "group name",
 "description": "what this group of api endpoints is for",
 }
 ],
 "paths": {
 "/api/v1/groupname/resource":
 .
 .
 .
 }
  40. @codeJENNerator

  41. @codeJENNerator

  42. @codeJENNerator • Swagger UI Approaches • Use SpringFox library •

    Copy static files and customize
  43. @codeJENNerator Considerations

  44. @codeJENNerator Customization img src: http://sergiodelamo.es/how-to-secure-your-grails-3-api-with-spring-security-rest-for-grails/

  45. @codeJENNerator • For any non-standard configuration, you may have to

    override the UI. • As one example, we were adding custom headers for oauth jwt tokens. At the time, it was not supported with springfox-ui.
  46. @codeJENNerator Object Mapping img src: https://github.com/springfox/springfox/issues/281

  47. @codeJENNerator • Along the same lines, the generated Swagger Spec

    doesn’t fully define complex and nested objects. • We ended up writing a lot of these object mappers.
  48. @codeJENNerator Hypermedia Support

  49. @codeJENNerator • In Swagger/OpenAPI Spec 2.0, there was no hypermedia

    support. • In OpenAPI Spec 3.0
  50. @codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3

    RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 }
  51. @codeJENNerator Annotation Hell 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method

    = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 }
  52. @codeJENNerator Annotation Hell 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method

    = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 }
  53. @codeJENNerator Annotation Hell 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method

    = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 } X
  54. @codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3

    RequestMethod.POST) 4 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 5 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 6 settingsValue.fieldOne,
 7 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 8 new Double(settingsValue.value))]
 9 )
 10 api.saveUpdatedSetting(request)
 11 }
  55. @codeJENNerator Advantages

  56. @codeJENNerator Swagger UI “Try-It” Button

  57. @codeJENNerator Swagger UI “Try-It” Alternatives

  58. @codeJENNerator src: https://www.getpostman.com/img/v1/docs/run_btn_ux/run_btn_ux_1.png

  59. @codeJENNerator Curl -> curl 'http://localhost:8080/greetings' -i -H 'Content-Type: text/plain' HTTP/1.1

    200 X-Application-Context: application:8080 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 26 Jan 2017 13:28:19 GMT [{"id":1,"message":"Hello"},{"id":2,"message":"Hi"},{"id":3,"message":"Hola"},{"id":4,"message":"Olá"},{"id":5,"message":"Hej"}]
  60. @codeJENNerator Mixing Spring REST Docs and Swagger

  61. @codeJENNerator https://github.com/Swagger2Markup/swagger2markup Swagger2Markup

  62. @codeJENNerator

  63. @codeJENNerator

  64. @codeJENNerator FAIL! img src: http://www.elvenspirit.com/elf/wp- content/uploads/2011/10/IMG_3013.jpg

  65. @codeJENNerator AssertJ-Swagger https://github.com/RobWin/assertj-swagger FAIL! img src: http://www.elvenspirit.com/elf/wp- content/uploads/2011/10/IMG_3013.jpg

  66. @codeJENNerator Definitions Swagger (OpenAPI) RAML Testing MockMVC RestAssured Documentation AsciiDoc

    Markdown Wikis Swagger UI Swagger2Markup AssertJ- Swagger Contract-First
  67. @codeJENNerator Definitions Swagger (OpenAPI) RAML Testing MockMVC RestAssured Documentation AsciiDoc

    Markdown Wikis Swagger UI Swagger2Markup AssertJ- Swagger Contract-First Spring REST Docs
  68. @codeJENNerator Test-Driven Documentation Green Red Refactor

  69. @codeJENNerator Test-Driven Documentation

  70. @codeJENNerator Test-Driven Documentation Red

  71. @codeJENNerator Test-Driven Documentation Red

  72. @codeJENNerator Test-Driven Documentation Document Red

  73. @codeJENNerator Test-Driven Documentation Document Red

  74. @codeJENNerator Test-Driven Documentation Document Green Red

  75. @codeJENNerator Test-Driven Documentation Document Green Red

  76. @codeJENNerator Test-Driven Documentation Document Green Red Refactor

  77. @codeJENNerator Test-Driven Documentation Document Green Red Refactor

  78. @codeJENNerator Winning Solution https://flic.kr/p/5XiKxU

  79. @codeJENNerator Winning Solution - Ensures documentation matches implementation - Encourages

    writing more tests - Reduces duplication in docs and tests - Removes annotations from source
  80. @codeJENNerator Spring REST Docs

  81. @codeJENNerator Game Changers https://flic.kr/p/9Tiv3U

  82. @codeJENNerator Game Changers •Generated code snippets •Tests fail when documentation

    is missing or out-of-date •Rest APIs with Hypermedia •Ratpack •Dynamic routing doesn’t work with autogenerated Swagger libraries
  83. @codeJENNerator Getting Started projects.spring.io/spring-restdocs

  84. @codeJENNerator Getting Started •Start with reading the docs; The written

    docs are good! •Overview •Sponsored by Pivotal •Project Lead - Andy Wilkinson •Current Version - 1.2.1 released May 12 •Can work with many JVM languages including Java, Groovy, and even Kotlin
  85. @codeJENNerator Other Interesting Talks • Java & Spring Boot •

    Test-Driven Documentation with Spring REST Docs - Spring I/O 2016 Andy Wilkinson • Writing comprehensive and guaranteed up-to-date REST API documentation - SpringOne Platform 2016 Anders Evers
  86. @codeJENNerator Groovier Spring REST docs • Spring Boot • Ratpack

    • Grails
  87. @codeJENNerator

  88. @codeJENNerator

  89. @codeJENNerator

  90. @codeJENNerator Groovier Spring REST docs Example - Spring Boot

  91. @codeJENNerator Groovier Spring REST docs Example - Spring Boot •

    Groovy Spring Boot Project
  92. @codeJENNerator Groovier Spring REST docs Example - Spring Boot •

    Groovy Spring Boot Project + Asciidoctor Gradle plugin
  93. @codeJENNerator Groovier Spring REST docs Example - Spring Boot •

    Groovy Spring Boot Project + Asciidoctor Gradle plugin + MockMVC and documentation to Spock tests
  94. @codeJENNerator Groovier Spring REST docs Example - Spring Boot •

    Groovy Spring Boot Project + Asciidoctor Gradle plugin + MockMVC and documentation to Spock tests + Add to static assets during build and publish to GitHub pages
  95. @codeJENNerator Groovy Spring Boot App • Start with lazybones spring

    boot app • Add mock endpoints for example https://github.com/jlstrater/groovy-spring-boot-restdocs-example
  96. @codeJENNerator Endpoints @CompileStatic
 @RestController
 @RequestMapping('/greetings')
 @Slf4j
 class GreetingsController { 


    @RequestMapping(method = RequestMethod.GET, produces = 'application/json')
 ResponseEntity<?> list(@RequestParam(required = false) String message) {
 … }
 
 @RequestMapping(path= '/{id}', method = RequestMethod.GET, produces = 'application/json')
 ResponseEntity<?> getById(@PathVariable String id) {
 … }
 
 @RequestMapping(method = RequestMethod.POST, produces = 'application/json', consumes = 'application/json')
 ResponseEntity<?> post(@RequestBody Greeting example) {
 … }
 }
  97. @codeJENNerator Endpoints @CompileStatic
 @RestController
 @RequestMapping('/greetings')
 @Slf4j
 class GreetingsController { 


    @RequestMapping(method = RequestMethod.GET, produces = 'application/json')
 ResponseEntity<?> list(@RequestParam(required = false) String message) {
 … }
 
 @RequestMapping(path= '/{id}', method = RequestMethod.GET, produces = 'application/json')
 ResponseEntity<?> getById(@PathVariable String id) {
 … }
 
 @RequestMapping(method = RequestMethod.POST, produces = 'application/json', consumes = 'application/json')
 ResponseEntity<?> post(@RequestBody Greeting example) {
 … }
 } Get all greetings or search by name /greetings or /greetings?message=Hello
  98. @codeJENNerator Endpoints @CompileStatic
 @RestController
 @RequestMapping('/greetings')
 @Slf4j
 class GreetingsController { 


    @RequestMapping(method = RequestMethod.GET, produces = 'application/json')
 ResponseEntity<?> list(@RequestParam(required = false) String message) {
 … }
 
 @RequestMapping(path= '/{id}', method = RequestMethod.GET, produces = 'application/json')
 ResponseEntity<?> getById(@PathVariable String id) {
 … }
 
 @RequestMapping(method = RequestMethod.POST, produces = 'application/json', consumes = 'application/json')
 ResponseEntity<?> post(@RequestBody Greeting example) {
 … }
 } Get all greetings or search by name /greetings or /greetings?message=Hello Get a greeting by id /greetings/1
  99. @codeJENNerator Endpoints @CompileStatic
 @RestController
 @RequestMapping('/greetings')
 @Slf4j
 class GreetingsController { 


    @RequestMapping(method = RequestMethod.GET, produces = 'application/json')
 ResponseEntity<?> list(@RequestParam(required = false) String message) {
 … }
 
 @RequestMapping(path= '/{id}', method = RequestMethod.GET, produces = 'application/json')
 ResponseEntity<?> getById(@PathVariable String id) {
 … }
 
 @RequestMapping(method = RequestMethod.POST, produces = 'application/json', consumes = 'application/json')
 ResponseEntity<?> post(@RequestBody Greeting example) {
 … }
 } Get all greetings or search by name /greetings or /greetings?message=Hello Get a greeting by id /greetings/1 Create a new greeting /greetings
  100. @codeJENNerator

  101. @codeJENNerator

  102. @codeJENNerator Asciidoctor Gradle Plugin

  103. @codeJENNerator Example AsciiDoc = Gr8Data API Guide
 Jenn Strater;
 :doctype:

    book
 :icons: font
 :source-highlighter: highlightjs
 :toc: left
 :toclevels: 4
 :sectlinks:
 
 [introduction]
 = Introduction
 
 The Gr8Data API is a RESTful web service for aggregating and displaying gender ratios from various companies across the world. This document outlines how to submit data from your company or team and
 how to access the aggregate data.
 
 [[overview-http-verbs]]
 == HTTP verbs
 
 The Gr8Data API tries to adhere as closely as possible to standard HTTP and REST conventions in its use of HTTP verbs.
  104. @codeJENNerator Install classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3'
 apply plugin: 'org.asciidoctor.convert'
 
 asciidoctor {


    sourceDir = file('src/docs')
 outputDir "$projectDir/src/main/resources/public"
 backends 'html5'
 attributes 'source-highlighter' : 'prettify',
 'imagesdir':'images',
 'toc':'left',
 'icons': 'font',
 'setanchors':'true',
 'idprefix':'',
 'idseparator':'-',
 'docinfo1':'true'
 }

  105. @codeJENNerator

  106. @codeJENNerator Converted to HTML

  107. @codeJENNerator

  108. @codeJENNerator

  109. @codeJENNerator MockMvc Test src: http://www.javacodegeeks.com/2013/04/spring-mvc-introduction-in-testing.html

  110. @codeJENNerator Stand Alone Setup class ExampleControllerSpec extends Specification {
 protected

    MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup( new ExampleController()).build()
 }
 
 void 'test and document get by message’() {
 when:
 ResultActions result = this.mockMvc.perform(get('/greetings')
 .param('message', ‘Hallo API Conference DE!’)
 .contentType(MediaType.APPLICATION_JSON)) then:
 result
 .andExpect(status().isOk())
 .andExpect(jsonPath(‘message’).value(‘Hallo API Conference DE!’)) } }
  111. @codeJENNerator Web Context Setup void setup() {
 this.mockMvc = MockMvcBuilders

    .webAppContextSetup(this.context) .build()
 }
  112. @codeJENNerator Web Context Setup void setup() {
 this.mockMvc = MockMvcBuilders

    .webAppContextSetup(this.context) .build()
 } If context is null, remember to add spock-spring!!
  113. @codeJENNerator

  114. @codeJENNerator

  115. @codeJENNerator Gradle ext {
 snippetsDir = file('build/generated-snippets')
 } 
 testCompile

    “org.springframework.restdocs:spring-restdocs-mockmvc:$ {springRestDocsVersion}" ext['spring-restdocs.version'] = '1.2.0.RELEASE'
  116. @codeJENNerator Gradle ext {
 snippetsDir = file('build/generated-snippets')
 } 
 testCompile

    “org.springframework.restdocs:spring-restdocs-mockmvc:$ {springRestDocsVersion}" ext['spring-restdocs.version'] = '1.2.0.RELEASE' Spring Boot Specific!
  117. @codeJENNerator asciidoctor {
 dependsOn test
 sourceDir = file('src/docs')
 outputDir "$projectDir/src/main/resources/public"


    + inputs.dir snippetsDir
 backends 'html5'
 attributes 'source-highlighter' : 'prettify',
 'imagesdir':'images',
 'toc':'left',
 'icons': 'font',
 'setanchors':'true',
 'idprefix':'',
 'idseparator':'-',
 'docinfo1':'true',
 + 'snippets': snippetsDir
 } Gradle
  118. @codeJENNerator With AsciiDoctor Module

  119. @codeJENNerator Gradle ext {
 snippetsDir = file('build/generated-snippets')
 } 
 testCompile

    “org.springframework.restdocs:spring-restdocs-mockmvc:${springRestDocsVersion}" asciidoctor “org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}" 
 test {
 outputs.dir snippetsDir
 }
 
 asciidoctor {
 dependsOn test
 inputs.dir snippetsDir
 }
  120. @codeJENNerator Gradle ext {
 snippetsDir = file('build/generated-snippets')
 } 
 testCompile

    “org.springframework.restdocs:spring-restdocs-mockmvc:${springRestDocsVersion}" asciidoctor “org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}" 
 test {
 outputs.dir snippetsDir
 }
 
 asciidoctor {
 dependsOn test
 inputs.dir snippetsDir
 }
  121. @codeJENNerator

  122. @codeJENNerator class GreetingsControllerSpec extends Specification {
 
 @Rule
 JUnitRestDocumentation restDocumentation

    = new JUnitRestDocumentation()
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 }
  123. @codeJENNerator class GreetingsControllerSpec extends Specification {
 
 @Rule
 JUnitRestDocumentation restDocumentation

    = new JUnitRestDocumentation()
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 }
  124. @codeJENNerator class GreetingsControllerSpec extends Specification {
 
 @Rule
 JUnitRestDocumentation restDocumentation

    = new JUnitRestDocumentation()
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 }
  125. @codeJENNerator class GreetingsControllerSpec extends Specification {
 
 @Rule
 JUnitRestDocumentation restDocumentation

    = new JUnitRestDocumentation()
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 } void 'test and document get by message'() {
 
 when:
 ResultActions result = this.mockMvc.perform(get('/greetings')
 .param('message', ‘Hello')
 .contentType(MediaType.APPLICATION_JSON))
  126. @codeJENNerator class GreetingsControllerSpec extends Specification {
 
 @Rule
 JUnitRestDocumentation restDocumentation

    = new JUnitRestDocumentation()
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 } then:
 result
 .andExpect(status().isOk())
 .andExpect(jsonPath(‘message').value('Hello')) .andDo(document('greetings-get-example', preprocessResponse(prettyPrint()), responseFields(greeting))) } FieldDescriptor[] greeting = new FieldDescriptor().with {
 [fieldWithPath('id').type(JsonFieldType.NUMBER)
 .description("The greeting's id"),
 fieldWithPath('message').type(JsonFieldType.STRING)
 .description("The greeting's message")]
 }
 } void 'test and document get by message'() {
 
 when:
 ResultActions result = this.mockMvc.perform(get('/greetings')
 .param('message', ‘Hello')
 .contentType(MediaType.APPLICATION_JSON))
  127. @codeJENNerator class GreetingsControllerSpec extends Specification {
 
 @Rule
 JUnitRestDocumentation restDocumentation

    = new JUnitRestDocumentation()
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 } then:
 result
 .andExpect(status().isOk())
 .andExpect(jsonPath(‘message').value('Hello')) .andDo(document('greetings-get-example', preprocessResponse(prettyPrint()), responseFields(greeting))) } FieldDescriptor[] greeting = new FieldDescriptor().with {
 [fieldWithPath('id').type(JsonFieldType.NUMBER)
 .description("The greeting's id"),
 fieldWithPath('message').type(JsonFieldType.STRING)
 .description("The greeting's message")]
 }
 } void 'test and document get by message'() {
 
 when:
 ResultActions result = this.mockMvc.perform(get('/greetings')
 .param('message', ‘Hello')
 .contentType(MediaType.APPLICATION_JSON))
  128. @codeJENNerator class GreetingsControllerSpec extends Specification {
 
 @Rule
 JUnitRestDocumentation restDocumentation

    = new JUnitRestDocumentation()
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 } then:
 result
 .andExpect(status().isOk())
 .andExpect(jsonPath(‘message').value('Hello')) .andDo(document('greetings-get-example', preprocessResponse(prettyPrint()), responseFields(greeting))) } FieldDescriptor[] greeting = new FieldDescriptor().with {
 [fieldWithPath('id').type(JsonFieldType.NUMBER)
 .description("The greeting's id"),
 fieldWithPath('message').type(JsonFieldType.STRING)
 .description("The greeting's message")]
 }
 } void 'test and document get by message'() {
 
 when:
 ResultActions result = this.mockMvc.perform(get('/greetings')
 .param('message', ‘Hello')
 .contentType(MediaType.APPLICATION_JSON))
  129. @codeJENNerator Special Use Case

  130. @codeJENNerator Setup Via Annotation 
 +@WebMvcTest(controllers = GreetingsController)
 +@AutoConfigureRestDocs(
 +

    outputDir = "build/generated-snippets",
 + uriHost = “greetingsfromcodeeurope.pl",
 + uriPort = 8080
 ) class BaseControllerSpec extends Specification { 
 // @Rule
 // JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/ docs/generated-snippets')
 
 + @Autowired
 protected MockMvc mockMvc
 //
 // @Autowired
 // private WebApplicationContext context
 //
 // void setup() {
 // this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
 // .apply(documentationConfiguration(this.restDocumentation))
 // .build()
 // } }
  131. @codeJENNerator

  132. @codeJENNerator

  133. @codeJENNerator Generated Snippets By Default When Specified curl-request.adoc response-fields.adoc http-request.adoc

    request-parameters.adoc httpie-request.adoc request-parts.adoc http-response.adoc request body response body
  134. @codeJENNerator Example Http Response [source,http,options="nowrap"]
 ----
 HTTP/1.1 200 OK
 Content-Type:

    application/json;charset=UTF-8
 Content-Length: 37
 
 {
 "id" : 1,
 "message" : "Hello"
 }
 ----
  135. @codeJENNerator Example Response Fields |===
 |Path|Type|Description
 
 |`id`
 |`Number`
 |The

    greeting's id
 
 |`message`
 |`String`
 |The greeting's message
 
 |===
  136. @codeJENNerator

  137. @codeJENNerator Create a new greeting void 'test and document post

    with example endpoint and custom name'() {
 when:
 ResultActions result = this.mockMvc.perform(post('/greetings')
 .content(new ObjectMapper().writeValueAsString( new Greeting(message: ‘Hallo API Conference DE!')))
 .contentType(MediaType.APPLICATION_JSON))
 then:
 result
 .andExpect(status().isCreated())
 .andDo(document('greetings-post-example', requestFields([fieldWithPath(‘id').type(JsonFieldType.NULL) .optional().description("The greeting's id"),
 fieldWithPath('message').type(JsonFieldType.STRING)
 .description("The greeting's message")])
 ))
 }
  138. @codeJENNerator Create a new greeting void 'test and document post

    with example endpoint and custom name'() {
 when:
 ResultActions result = this.mockMvc.perform(post('/greetings')
 .content(new ObjectMapper().writeValueAsString( new Greeting(message: ‘Hallo API Conference DE!')))
 .contentType(MediaType.APPLICATION_JSON))
 then:
 result
 .andExpect(status().isCreated())
 .andDo(document('greetings-post-example', requestFields([fieldWithPath(‘id').type(JsonFieldType.NULL) .optional().description("The greeting's id"),
 fieldWithPath('message').type(JsonFieldType.STRING)
 .description("The greeting's message")])
 ))
 }
  139. @codeJENNerator List Greetings void 'test and document get of a

    list of greetings'() {
 when:
 ResultActions result = this.mockMvc.perform(get('/greetings')
 .contentType(MediaType.TEXT_PLAIN))
 
 then:
 result
 .andExpect(status().isOk())
 .andDo(document(‘greetings-list-example', preprocessResponse(prettyPrint()),
 responseFields(greetingList)
 ))
 } FieldDescriptor[] greetingList = new FieldDescriptor().with {
 [fieldWithPath('[].id').type(JsonFieldType.NUMBER).optional()
 .description("The greeting's id"),
 fieldWithPath('[].message').type(JsonFieldType.STRING)
 .description("The greeting's message")]
 }
  140. @codeJENNerator List Greetings void 'test and document get of a

    list of greetings'() {
 when:
 ResultActions result = this.mockMvc.perform(get('/greetings')
 .contentType(MediaType.TEXT_PLAIN))
 
 then:
 result
 .andExpect(status().isOk())
 .andDo(document(‘greetings-list-example', preprocessResponse(prettyPrint()),
 responseFields(greetingList)
 ))
 } FieldDescriptor[] greetingList = new FieldDescriptor().with {
 [fieldWithPath('[].id').type(JsonFieldType.NUMBER).optional()
 .description("The greeting's id"),
 fieldWithPath('[].message').type(JsonFieldType.STRING)
 .description("The greeting's message")]
 }
  141. @codeJENNerator Path Parameters void 'test and document getting a greeting

    by id'() {
 when:
 ResultActions result = this.mockMvc.perform( RestDocumentationRequestBuilders.get('/greetings/{id}', 1))
 
 then:
 result
 .andExpect(status().isOk())
 .andExpect(jsonPath('id').value(1))
 .andDo(document('greetings-get-by-id-example',
 preprocessResponse(prettyPrint()),
 pathParameters(parameterWithName(‘id') .description("The greeting's id")),
 responseFields(greeting)
 ))
 }
  142. @codeJENNerator Path Parameters void 'test and document getting a greeting

    by id'() {
 when:
 ResultActions result = this.mockMvc.perform( RestDocumentationRequestBuilders.get('/greetings/{id}', 1))
 
 then:
 result
 .andExpect(status().isOk())
 .andExpect(jsonPath('id').value(1))
 .andDo(document('greetings-get-by-id-example',
 preprocessResponse(prettyPrint()),
 pathParameters(parameterWithName(‘id') .description("The greeting's id")),
 responseFields(greeting)
 ))
 }
  143. @codeJENNerator

  144. @codeJENNerator Failing Tests

  145. @codeJENNerator Failing Tests

  146. @codeJENNerator Failing Tests

  147. @codeJENNerator +

  148. @codeJENNerator +

  149. @codeJENNerator Add Snippets to AsciiDoc == Errors
 
 Whenever an

    error response (status code >= 400) is returned, the body will contain a JSON object
 that describes the problem. The error object has the following structure:
 
 include::{snippets}/error-example/response-fields.adoc[]
 
 For example, a request that attempts to apply a non-existent tag to a note will produce a
 `400 Bad Request` response:
 
 include::{snippets}/error-example/http-response.adoc[]
 
 [[resources]]
 = Resources
 
 include::resources/example.adoc[]
  150. @codeJENNerator New in 1.2!

  151. @codeJENNerator

  152. @codeJENNerator Build Docs src/docs index.adoc src/main/ resources/public/ html5 index.html

  153. @codeJENNerator Publishing Strategies

  154. @codeJENNerator Strategies • Hook in asciidoctor with the gradle build

    task • Run the asciidoctor test separately (but make sure to run AFTER the tests)
  155. @codeJENNerator

  156. @codeJENNerator http://api.example.com/docs

  157. @codeJENNerator Publish Docs to Github Pages publish.gradle buildscript {
 repositories

    {
 jcenter()
 }
 
 dependencies {
 classpath 'org.ajoberstar:gradle-git:1.1.0'
 }
 }
 
 apply plugin: 'org.ajoberstar.github-pages'
 
 githubPages {
 repoUri = 'git@github.com:jlstrater/groovy-spring-boot-restdocs-example.git'
 pages {
 from(file('build/resources/main/public/html5')) }
 }
  158. @codeJENNerator Publish Docs to Github Pages publish.gradle buildscript {
 repositories

    {
 jcenter()
 }
 
 dependencies {
 classpath 'org.ajoberstar:gradle-git:1.1.0'
 }
 }
 
 apply plugin: 'org.ajoberstar.github-pages'
 
 githubPages {
 repoUri = 'git@github.com:jlstrater/groovy-spring-boot-restdocs-example.git'
 pages {
 from(file('build/resources/main/public/html5')) }
 } If you use this method, remember to deploy docs at the same time as the project!
  159. @codeJENNerator

  160. @codeJENNerator http://jlstrater.github.io/groovy-spring-boot-restdocs-example

  161. @codeJENNerator Support for Rest-Assured

  162. @codeJENNerator Groovier Spring REST docs • Ratpack • Grails

  163. @codeJENNerator

  164. @codeJENNerator Groovier Spring REST docs Example - Ratpack • Ratpack

    Example Project • https://github.com/ratpack/example-books • Spring RESTdocs RestAssured • https://github.com/ratpack/example-books/pull/25
  165. @codeJENNerator

  166. @codeJENNerator

  167. @codeJENNerator restdocs.gradle dependencies {
 testCompile ‘org.springframework.restdocs:spring-restdocs-restassured:${springRestDocsVersion}'
 }
 
 ext {


    snippetsDir = file('build/generated-snippets')
 } test {
 outputs.dir snippetsDir
 }
 
 asciidoctor {
 mustRunAfter test
 inputs.dir snippetsDir
 sourceDir = file('src/docs')
 separateOutputDirs = false
 outputDir "$projectDir/src/ratpack/public/docs"
 attributes 'snippets': snippetsDir,
 'source-highlighter': 'prettify',
 'imagesdir': 'images',
 'toc': 'left',
 'icons': 'font',
 'setanchors': 'true',
 'idprefix': '',
 'idseparator': '-',
 'docinfo1': 'true'
 }
 
 build.dependsOn asciidoctor

  168. @codeJENNerator build.gradle plugins {
 id "io.ratpack.ratpack-groovy" version "1.2.0"
 . .

    .
 + id 'org.asciidoctor.convert' version '1.5.3'
 }
 
 repositories {
 jcenter() . . . } 
 //some CI config
 apply from: "gradle/ci.gradle"
 + apply from: "gradle/restdocs.gradle"

  169. @codeJENNerator

  170. @codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {


    get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 }
 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List<Book> books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } }
  171. @codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {


    get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 }
 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List<Book> books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Get a book by ISBN /api/books/1932394842
  172. @codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {


    get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 }
 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List<Book> books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Get a book by ISBN /api/books/1932394842 Get all books /api/books
  173. @codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {


    get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 }
 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List<Book> books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Get a book by ISBN /api/books/1932394842 Get all books /api/books Post to create a new book /api/books
  174. @codeJENNerator

  175. @codeJENNerator Asciidoctor Gradle Plugin (same as before)

  176. @codeJENNerator

  177. @codeJENNerator https://github.com/jayway/rest-assured

  178. @codeJENNerator abstract class BaseDocumentationSpec extends Specification {
 
 @Shared
 ApplicationUnderTest

    aut = new ExampleBooksApplicationUnderTest()
 
 @Rule
 JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()
 
 protected RequestSpecification documentationSpec
 
 void setup() {
 this.documentationSpec = new RequestSpecBuilder()
 .addFilter(documentationConfiguration(restDocumentation))
 .build()
 }
 }

  179. @codeJENNerator class BookDocumentationSpec extends BaseDocumentationSpec {
 
 @Shared
 EmbeddedApp isbndb

    = GroovyEmbeddedApp.of {
 handlers {
 all {
 render '{"data" : [{"title" : "Learning Ratpack", "publisher_name" : "O\'Reilly Media", "author_data" : [{"id" : "dan_woods", "name" : "Dan Woods"}]}]}'
 }
 }
 }
 
 @Delegate
 TestHttpClient client = aut.httpClient
 RemoteControl remote = new RemoteControl(aut)
 
 
 def setupSpec() {
 System.setProperty('eb.isbndb.host', "http://${isbndb.address.host}:${isbndb.address.port}")
 System.setProperty('eb.isbndb.apikey', "fakeapikey")
 }
 
 def cleanupSpec() {
 System.clearProperty('eb.isbndb.host')
 }
 
 def setupTestBook() {
 requestSpec { RequestSpec requestSpec ->
 requestSpec.body.type("application/json")
 requestSpec.body.text(JsonOutput.toJson([isbn: "1932394842", quantity: 0, price: 22.34]))
 }
 post("api/book")
 }
 

  180. @codeJENNerator class BookDocumentationSpec extends BaseDocumentationSpec {
 
 @Shared
 EmbeddedApp isbndb

    = GroovyEmbeddedApp.of {
 handlers {
 all {
 render '{"data" : [{"title" : "Learning Ratpack", "publisher_name" : "O\'Reilly Media", "author_data" : [{"id" : "dan_woods", "name" : "Dan Woods"}]}]}'
 }
 }
 }
 
 @Delegate
 TestHttpClient client = aut.httpClient
 RemoteControl remote = new RemoteControl(aut)
 
 
 def setupSpec() {
 System.setProperty('eb.isbndb.host', "http://${isbndb.address.host}:${isbndb.address.port}")
 System.setProperty('eb.isbndb.apikey', "fakeapikey")
 }
 
 def cleanupSpec() {
 System.clearProperty('eb.isbndb.host')
 }
 
 def setupTestBook() {
 requestSpec { RequestSpec requestSpec ->
 requestSpec.body.type("application/json")
 requestSpec.body.text(JsonOutput.toJson([isbn: "1932394842", quantity: 0, price: 22.34]))
 }
 post("api/book")
 }
 
 Setup Test Data and Cleanup After Each Test
  181. @codeJENNerator void 'test and document list books'() {
 setup:
 setupTestBook()


    
 expect:
 given(this.documentationSpec)
 .contentType('application/json')
 .accept('application/json')
 .port(aut.address.port)
 .filter(document('books-list-example',
 preprocessRequest(modifyUris()
 .host('books.example.com')
 .removePort()),
 preprocessResponse(prettyPrint()),
 responseFields(
 fieldWithPath('[].isbn').description('The ISBN of the book'),
 fieldWithPath('[].quantity').description("The quantity of the book that is available"),
 fieldWithPath('[].price').description("The current price of the book"),
 fieldWithPath('[].title').description("The title of the book"),
 fieldWithPath('[].author').description('The author of the book'),
 fieldWithPath('[].publisher').description('The publisher of the book')
 )))
 .when()
 .get('/api/book')
 .then()
 .assertThat()
 .statusCode(is(200))
 }
  182. @codeJENNerator Response Body Snippet [source,options="nowrap"]
 ----
 [ {
 "isbn" :

    "1932394842",
 "quantity" : 0,
 "price" : 22.34,
 "title" : "Learning Ratpack",
 "author" : "Dan Woods",
 "publisher" : "O'Reilly Media"
 } ]
 ----
  183. @codeJENNerator

  184. @codeJENNerator

  185. @codeJENNerator Reusable Snippets protected final Snippet getBookFields() {
 responseFields(
 fieldWithPath('isbn').description('The

    ISBN of the book'),
 fieldWithPath('quantity').description("The quantity of the book that is available"),
 fieldWithPath('price').description("The current price of the book"),
 fieldWithPath('title').description("The title of the book"),
 fieldWithPath('author').description('The author of the book’), fieldWithPath('publisher').description('The publisher of the book’) )
 }
  186. @codeJENNerator def "test and document create book"() {
 given:
 def

    setup = given(this.documentationSpec)
 .body('{"isbn": "1234567890", "quantity": 10, "price": 22.34}')
 .contentType('application/json')
 .accept('application/json')
 .port(aut.address.port)
 .filter(document('books-create-example',
 preprocessRequest(prettyPrint(),
 modifyUris()
 .host('books.example.com')
 .removePort()),
 preprocessResponse(prettyPrint()),
 bookFields,
 requestFields(
 fieldWithPath('isbn').type(JsonFieldType.STRING).description('book ISBN id'),
 fieldWithPath('quantity').type(JsonFieldType.NUMBER).description('quanity available'),
 fieldWithPath('price').type(JsonFieldType.NUMBER)
 .description('price of the item as a number without currency')
 ),))
 when:
 def result = setup
 .when()
 .post("api/book")
 then:
 result
 .then()
 .assertThat()
 .statusCode(is(200))
 }
  187. @codeJENNerator def "test and document create book"() {
 given:
 def

    setup = given(this.documentationSpec)
 .body('{"isbn": "1234567890", "quantity": 10, "price": 22.34}')
 .contentType('application/json')
 .accept('application/json')
 .port(aut.address.port)
 .filter(document('books-create-example',
 preprocessRequest(prettyPrint(),
 modifyUris()
 .host('books.example.com')
 .removePort()),
 preprocessResponse(prettyPrint()),
 bookFields,
 requestFields(
 fieldWithPath('isbn').type(JsonFieldType.STRING).description('book ISBN id'),
 fieldWithPath('quantity').type(JsonFieldType.NUMBER).description('quanity available'),
 fieldWithPath('price').type(JsonFieldType.NUMBER)
 .description('price of the item as a number without currency')
 ),))
 when:
 def result = setup
 .when()
 .post("api/book")
 then:
 result
 .then()
 .assertThat()
 .statusCode(is(200))
 }
  188. @codeJENNerator Public APIs

  189. @codeJENNerator Blog Post

  190. @codeJENNerator Outcomes

  191. @codeJENNerator One Year Later • Made it to production! :)

    • Team still happy with Spring REST Docs • Other dev teams like to see the examples
  192. @codeJENNerator Read the docs for more on.. • Adding Security

    and Headers • Documenting Constraints • Hypermedia Support • XML Support • Using Markdown instead of Asciidoc • Third Party Extensions for WireMock and Jersey
  193. @codeJENNerator Conclusion

  194. @codeJENNerator Definitions Swagger (OpenAPI) RAML Testing MockMVC RestAssured Documentation AsciiDoc

    Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First
  195. @codeJENNerator

  196. @codeJENNerator • API documentation is complex

  197. @codeJENNerator • API documentation is complex • Choosing the right

    tool for the job not just about the easiest one to setup
  198. @codeJENNerator • API documentation is complex • Choosing the right

    tool for the job not just about the easiest one to setup • Spring REST Docs is a promising tool to enforce good testing and documentation practices without muddying source code.
  199. @codeJENNerator • API documentation is complex • Choosing the right

    tool for the job not just about the easiest one to setup • Spring REST Docs is a promising tool to enforce good testing and documentation practices without muddying source code. • I still hate writing boilerplate documentation, but at least it’s a little less painful now.
  200. @codeJENNerator Next Steps • Join the Groovy Community on Slack

    groovycommunity.com • Join #spring-restdocs on gitter https://gitter.im/spring-projects/spring-restdocs
  201. @codeJENNerator Questions? https://github.com/jlstrater/groovy-spring-boot-restdocs-example https://github.com/ratpack/example-books https://github.com/jlstrater/spring-restdocs-public-api-example https://flic.kr/p/5DeuzB