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

Test Driven Docs CEKrakow 2017

Test Driven Docs CEKrakow 2017

Test-driven documentation solutions, such as Spring Rest Docs, generate example snippets for requests and responses from the tests ensuring both code coverage and accurate documentation. This session will walk through how to implement such solutions for Spring Boot, Grails, and Ratpack.

------------

Jennifer “Jenn” Strater is the co-founder of the organization Gr8Ladies. She has also organized Gr8Workshops for developers interested in an overview and crash course in Groovy technologies. She has presented on several Groovy topics at events such as the Grace Hopper Celebration of Women in Computing, Greach, Gr8Conf EU, Gr8Conf US, and Devoxx Belgium. In August 2016, Jenn started in the master’s program at the Technical University of Denmark(DTU). She is studying static analysis and compilers with a focus on Groovy with funding from the Fulbright U.S. Student Program.

jlstrater

April 26, 2017
Tweet

More Decks by jlstrater

Other Decks in Technology

Transcript

  1. A Test-Driven Approach to Documenting RESTful APIs with Groovy and

    Spring REST Docs 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. Click the ‘download pdf’ button on the right.
  3. @codeJENNerator https://speakerdeck.com/jlstrater/test-driven-docs-cekrakow-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

  4. @codeJENNerator About Me

  5. @codeJENNerator About Me Starting at Zenjob in Berlin in June

    2017!
  6. @codeJENNerator About Me - Taking classes at the Technical University

    of Denmark and working on a research project - Also exploring Danish Culture with funding from the US Fulbright Grant program - Prior to the Fulbright Grant, I was a senior consultant at Object Partners, Inc. in Minneapolis, MN, USA. My work there 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. - Moving to Berlin and starting at Zenjob in June 2017.
  7. @codeJENNerator Background

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

    Ratpack • Grails
  9. @codeJENNerator Background • Creating RESTful APIs • Spring Boot •

    Ratpack • Grails • Documentation • Swagger • 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

  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. None
  17. @codeJENNerator Central Information Security Http Verbs Error Handling Http Status

  18. @codeJENNerator Endpoint Vs Resource Design

  19. @codeJENNerator Endpoint Vs Resource Design VS

  20. @codeJENNerator Swagger

  21. @codeJENNerator Swagger Approaches

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

  23. @codeJENNerator Super Easy Install • io.springfox:springfox-swagger2 • io.springfox:springfox-swagger-ui

  24. @codeJENNerator Custom Swagger Specification

  25. @codeJENNerator Custom JSON {
 "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":
 .
 .
 .
 }
  26. @codeJENNerator Swagger UI • Springfox generated UI • Copy static

    files and customize
  27. @codeJENNerator Considerations

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

  29. @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 }
  30. @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 }
  31. @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 }
  32. @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
  33. @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 }
  34. @codeJENNerator Object Mapping img src: https://github.com/springfox/springfox/issues/281

  35. @codeJENNerator Advantages

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

  37. @codeJENNerator The Alternatives

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

  39. @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"}]
  40. @codeJENNerator Mixing Spring REST Docs and Swagger

  41. @codeJENNerator Swagger2Markup https://swagger2markup.readme.io/

  42. None
  43. None
  44. @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

  45. @codeJENNerator Test-Driven Documentation Green Red Refactor

  46. @codeJENNerator Test-Driven Documentation

  47. @codeJENNerator Test-Driven Documentation Red

  48. @codeJENNerator Test-Driven Documentation Red

  49. @codeJENNerator Test-Driven Documentation Document Red

  50. @codeJENNerator Test-Driven Documentation Document Red

  51. @codeJENNerator Test-Driven Documentation Document Green Red

  52. @codeJENNerator Test-Driven Documentation Document Green Red

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

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

  55. Winning Solution https://flic.kr/p/5XiKxU

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

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

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

  59. @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 Swagger
  60. @codeJENNerator Getting Started

  61. @codeJENNerator Overview projects.spring.io/spring-restdocs springrestdocs

  62. @codeJENNerator Overview •Sponsored by Pivotal •Project Lead - Andy Wilkinson

    •Current Last Version - 1.2.0 released Monday April 24 •Most Recent Bug Fix Version - 1.1.3 released Monday April 24 •*NEW* Twitter Account and Official Logo
  63. @codeJENNerator Other Interesting Talks • Documenting RESTful Apis - SpringOne2GX

    2015 Andy Wilkinson • Writing comprehensive and guaranteed up-to-date REST API documentation - SpringOne Platform 2016 Anders Evers • Documenting APIs with Spring REST Docs - Tomasz Kopczynski @t_kopczynski
  64. @codeJENNerator Release Highlights

  65. 1.0.0

  66. 1.0.0 MockMVC

  67. 1.0.0 MockMVC AutoGenerated Snippets

  68. 1.0.0 MockMVC AutoGenerated Snippets AsciiDoctor Integration

  69. 1.0.0 MockMVC AutoGenerated Snippets AsciiDoctor Integration Hypermedia Support

  70. 1.1.0 img src: https://flic.kr/p/bwVxge

  71. Relaxed and Reusable Snippets 1.1.0 img src: https://flic.kr/p/bwVxge RestAssured

  72. Relaxed and Reusable Snippets Markdown 1.1.0 img src: https://flic.kr/p/bwVxge RestAssured

    TestNG
  73. @codeJENNerator Upgrading to 1.1 • RESTDocumentation is now JUnitRestDocumentation or

    ManualRestDocumentation • Specify special dependency for restassured
  74. AsciiDoctor Macro Support Complex Payloads img src: https://www.flickr.com/ photos/dskley/15558668690 1.2.0

    JUST RELEASED! Request Body Snippets Response Body Snippets
  75. @codeJENNerator Upgrading to 1.2 • If using Rest Assured 3

    • add dependency for testCompile "io.rest- assured:rest-assured:3.0.2" • REST Assured 3 changes the package from com.jayway.restassured to io.restassured • Change Spring REST docs support to restassured3 (will show deprecated in IntelliJ and the app will fail)
  76. @codeJENNerator AsciiDoctor Module • adds the default configuration in Gradle

    • removes some boilerplate from the asciidoc files • (see relevant slides)
  77. img src: https://flic.kr/p/S9wzGb 2.0

  78. Java 8+ Spring 5+ img src: https://flic.kr/p/S9wzGb 2.0 WebTestClient

  79. @codeJENNerator Groovier Spring REST docs • Spring Boot • Ratpack

    • Grails
  80. @codeJENNerator

  81. @codeJENNerator

  82. @codeJENNerator

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

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

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

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

    Groovy Spring Boot Project + Asciidoctor Gradle plugin + MockMVC and documentation to Spock tests
  87. @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
  88. @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
  89. @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) {
 … }
 }
  90. @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
  91. @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
  92. @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
  93. @codeJENNerator

  94. @codeJENNerator

  95. @codeJENNerator Asciidoctor Gradle Plugin

  96. @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.
  97. @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'
 }

  98. @codeJENNerator

  99. @codeJENNerator Converted to HTML

  100. @codeJENNerator

  101. @codeJENNerator

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

  103. @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', ‘Cześć Code Europe!’)
 .contentType(MediaType.APPLICATION_JSON)) then:
 result
 .andExpect(status().isOk())
 .andExpect(jsonPath(‘message’).value('Cześć Code Europe!’)) } }
  104. @codeJENNerator Web Context Setup void setup() {
 this.mockMvc = MockMvcBuilders

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

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

  107. @codeJENNerator

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

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

    “org.springframework.restdocs:spring- restdocs-mockmvc:${springRestDocsVersion}" ext['spring-restdocs.version'] = '1.2.0.RELEASE'
  110. @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!
  111. @codeJENNerator Gradle 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
 }
  112. @codeJENNerator With AsciiDoctor Module

  113. @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
 }
  114. @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
 }
  115. @codeJENNerator

  116. @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()
 }
  117. @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()
 }
  118. @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()
 }
  119. @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))
  120. @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))
  121. @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))
  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()
 } 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))
  123. @codeJENNerator Special Use Case

  124. @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()
 // } }
  125. @codeJENNerator

  126. @codeJENNerator

  127. @codeJENNerator Generated Snippets • curl-request.adoc • http-request.adoc • httpie-request.adoc •

    http-response.adoc • request body • response body • response-fields.adoc • request-parameters.adoc • request-parts.adoc
  128. @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"
 }
 ----
  129. @codeJENNerator Example Response Fields |===
 |Path|Type|Description
 
 |`id`
 |`Number`
 |The

    greeting's id
 
 |`message`
 |`String`
 |The greeting's message
 
 |===
  130. None
  131. @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: ‘Cześć Code Europe!')))
 .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")])
 ))
 }
  132. @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: ‘Cześć Code Europe!')))
 .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")])
 ))
 }
  133. @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")]
 }
  134. @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")]
 }
  135. @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)
 ))
 }
  136. @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)
 ))
 }
  137. @codeJENNerator

  138. @codeJENNerator Failing Tests

  139. @codeJENNerator Failing Tests

  140. @codeJENNerator Failing Tests

  141. @codeJENNerator +

  142. @codeJENNerator +

  143. @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[]
  144. @codeJENNerator New in 1.2!

  145. @codeJENNerator

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

  147. @codeJENNerator Strategies

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

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

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

  151. @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')) }
 }
  152. @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!
  153. @codeJENNerator

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

  155. @codeJENNerator Support for Rest-Assured

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

  157. @codeJENNerator

  158. @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
  159. @codeJENNerator

  160. @codeJENNerator

  161. @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

  162. @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"

  163. @codeJENNerator

  164. @codeJENNerator

  165. @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
 }
 } }
  166. @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
  167. @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
  168. @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
  169. @codeJENNerator

  170. @codeJENNerator

  171. Asciidoctor Gradle Plugin (same as before)

  172. @codeJENNerator

  173. @codeJENNerator

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

  175. @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()
 }
 }

  176. @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")
 }
 
 def cleanup() {
 remote.exec {
 get(Sql).execute("delete from books")
 }
 } . . .
  177. @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")
 }
 
 def cleanup() {
 remote.exec {
 get(Sql).execute("delete from books")
 }
 } . . . Setup Test Data and Cleanup After Each Test
  178. @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))
 }
  179. @codeJENNerator Response Body Snippet [source,options="nowrap"]
 ----
 [ {
 "isbn" :

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

  181. @codeJENNerator

  182. @codeJENNerator

  183. @codeJENNerator

  184. None
  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 https://jennstrater.blogspot.dk/2017/01/ using-spring-rest-docs-to-document.html

  190. @codeJENNerator

  191. @codeJENNerator Groovier Spring REST docs Sample - Grails

  192. @codeJENNerator Groovier Spring REST docs Sample - Grails • Grails

    3.2.6
  193. @codeJENNerator Groovier Spring REST docs Sample - Grails • Grails

    3.2.6 + Asciidoctor Gradle plugin
  194. @codeJENNerator Groovier Spring REST docs Sample - Grails • Grails

    3.2.6 + Asciidoctor Gradle plugin + Spring REST docs - RestAssured
  195. @codeJENNerator

  196. @codeJENNerator package com.example
 
 import grails.rest.Resource
 
 @Resource(uri='/notes', readOnly =

    false, formats = ['json', 'xml'])
 class Note {
 Long id
 String title
 String body
 
 static hasMany = [tags: Tag]
 static mapping = {
 tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ]
 }
 }
  197. @codeJENNerator package com.example import grails.rest.Resource
 @Resource(uri='/tags', readOnly = false, formats

    = ['json', 'xml'])
 class Tag {
 Long id
 String name
 
 static hasMany = [notes: Note]
 static belongsTo = Note
 static mapping = {
 notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id']
 }
 }
  198. @codeJENNerator

  199. @codeJENNerator

  200. Asciidoctor Gradle Plugin (same as before)

  201. @codeJENNerator

  202. @codeJENNerator

  203. @codeJENNerator https://github.com/jayway/rest-assured (same as before)

  204. @codeJENNerator Grails Specific Setup + testCompile “org.springframework.restdocs:spring-restdocs- restassured:${springRestDocsVersion}” asciidoctor {

    … + mustRunAfter integrationTest … }
  205. @codeJENNerator @Integration
 @Rollback
 class ApiDocumentationSpec extends Specification {
 @Rule
 JUnitRestDocumentation

    restDocumentation = new JUnitRestDocumentation()
 
 protected RequestSpecification documentationSpec
 
 void setup() {
 this.documentationSpec = new RequestSpecBuilder()
 .addFilter(documentationConfiguration(restDocumentation))
 .build()
 } }
  206. @codeJENNerator @Integration
 @Rollback
 class ApiDocumentationSpec extends Specification {
 @Rule
 JUnitRestDocumentation

    restDocumentation = new JUnitRestDocumentation()
 
 protected RequestSpecification documentationSpec
 
 void setup() {
 this.documentationSpec = new RequestSpecBuilder()
 .addFilter(documentationConfiguration(restDocumentation))
 .build()
 } }
  207. @codeJENNerator void 'test and document notes list request'() {
 expect:


    given(this.documentationSpec)
 .accept(MediaType.APPLICATION_JSON.toString())
 .filter(document('notes-list-example',
 preprocessRequest(modifyUris()
 .host('api.example.com')
 .removePort()),
 preprocessResponse(prettyPrint()),
 responseFields(
 fieldWithPath('[].class').description('the class of the resource'),
 fieldWithPath('[].id').description('the id of the note'),
 fieldWithPath('[].title').description('the title of the note'),
 fieldWithPath('[].body').description('the body of the note'),
 subsectionWithPath(‘tags').type(JsonFieldType.ARRAY) .description('a list of tags associated with the note'),
 )))
 .when()
 .port(8080)
 .get('/notes')
 .then()
 .assertThat()
 .statusCode(is(200))
 }
  208. @codeJENNerator void 'test and document notes list request'() {
 expect:


    given(this.documentationSpec)
 .accept(MediaType.APPLICATION_JSON.toString())
 .filter(document('notes-list-example',
 preprocessRequest(modifyUris()
 .host('api.example.com')
 .removePort()),
 preprocessResponse(prettyPrint()),
 responseFields(
 fieldWithPath('[].class').description('the class of the resource'),
 fieldWithPath('[].id').description('the id of the note'),
 fieldWithPath('[].title').description('the title of the note'),
 fieldWithPath('[].body').description('the body of the note'),
 subsectionWithPath(‘tags').type(JsonFieldType.ARRAY) .description('a list of tags associated with the note'),
 )))
 .when()
 .port(8080)
 .get('/notes')
 .then()
 .assertThat()
 .statusCode(is(200))
 }
  209. @codeJENNerator Response Body Snippet |===
 |Path|Type|Description
 
 |`id`
 |`Number`
 |the

    id of the note
 
 |`title`
 |`String`
 |the title of the note
 
 |`body`
 |`String`
 |the body of the note
 
 |`tags`
 |`Array`
 |the list of tags associated with the note
 
 |===
  210. @codeJENNerator

  211. None
  212. @codeJENNerator Outcomes

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

    • Team still happy with Spring REST Docs • Other dev teams like to see the examples
  214. @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
  215. @codeJENNerator Conclusion

  216. @codeJENNerator

  217. @codeJENNerator • API documentation is complex

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

    tool for the job not just about the easiest one to setup
  219. @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.
  220. @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.
  221. @codeJENNerator Next Steps • Join the Groovy Community on Slack

    groovycommunity.com • Join #spring-restdocs on gitter https://gitter.im/spring-projects/ spring-restdocs • Gr8Conf EU May 31 - June 2, 2017 in Copenhagen, Denmark gr8conf.eu
  222. @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