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

Test Driven Docs Warsaw JUG 2018

Test Driven Docs Warsaw JUG 2018

Documenting RESTful APIs with Spring REST Docs

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. Examples will be in Spring Boot and Groovy, but the concepts are applicable to other ecosystems too. If time permits, the talk will also include how to document APIs that have been implemented using Spring Framework 5's WebFlux. 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.

Speaker:

Jennifer “Jenn” Strater is a Senior Engineer at Zenjob Gmbh in Berlin, Germany where we revolutionize the way students work. She was formerly a co-founder of GR8Ladies and is now part of the board of GR8DI, the Apache Groovy Diversity Initiative. She has also organized GR8Workshops for developers interested in an overview and crash course in Groovy technologies and presented on several Groovy topics at events such as the Grace Hopper Celebration of Women in Computing, Greach, GR8Conf EU, GR8Conf US, Devoxx Belgium, JFokus, and Spring One Platform.

1f28a0c1988421be3268026bd6bb6f49?s=128

jlstrater

June 26, 2018
Tweet

More Decks by jlstrater

Other Decks in Technology

Transcript

  1. Documenting RESTful APIs with Spring REST Docs By Jenn Strater

    @codeJENNerator
  2. @codeJENNerator Notes For Those Viewing These 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 Outline • API Documentation Background • Approaches to Documentation

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

  5. @codeJENNerator About Me

  6. @codeJENNerator

  7. @codeJENNerator About Me • 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. • Senior Engineer at Zenjob as of June 2017. We’re hiring! zenjob.de/ careers • Spent the 2016-2017 academic 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 with Spring REST Docs started on a project at my client through them.
  8. @codeJENNerator Audience Background • Creating RESTful APIs • Spring Boot

    • Grails • Ratpack • API Documentation • Wiki Pages, Word Documents, Confluence, etc • Asciidoc / Asciidoctor • Swagger / RAML
  9. @codeJENNerator src: https://flic.kr/p/rehEf5

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

  11. @codeJENNerator Case Study

  12. REST Maturity Model src: http://martinfowler.com/articles/richardsonMaturityModel.html

  13. REST Maturity Model src: http://martinfowler.com/articles/richardsonMaturityModel.html

  14. @codeJENNerator REST Maturity Model • 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
  15. Documenting RESTfullike APIs with Spring REST Docs By Jenn Strater

    @codeJENNerator
  16. @codeJENNerator REST Maturity Model Attribution: @Alvaro_Sanchez

  17. @codeJENNerator Monolith vs Microservices • 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
  18. @codeJENNerator Central Information Security Http Verbs Error Handling Http Status

  19. @codeJENNerator Central Information • 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.
  20. @codeJENNerator Document Design

  21. @codeJENNerator URI Centric

  22. @codeJENNerator URI Centric

  23. @codeJENNerator Central Information • Who’s seen this before? Of the

    people who have never used swagger before, how many understand what this means? Even our CTO who is technical, didn’t want to spend time figuring it out. Also, product teams. • This is a swagger ui example but the concept is not limited to Swagger. I have seen many different APIs document in this way. It’s not just URI centric, but also very developer centric. No matter whether you leave here choosing Swagger or Spring REST Docs, think about your users! • This is an example from Spring Rest Docs using Asciidoc. • Notice the very different way of organizing information on the second slide. Resource centric document design organizes information by topic and includes urls in the examples only. The information from generated solutions isn’t enough. We need the handwritten information too!
  24. @codeJENNerator Available Tools

  25. None
  26. Definitions Swagger (OpenAPI Specification) RAML

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

  28. Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis Swagger

    UI Swagger2Markup
  29. Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc

    Markdown Wikis Swagger UI Swagger2Markup
  30. Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc

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

    Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First
  32. Definitions Swagger (OpenAPI Specification) RAML

  33. None
  34. The specification formerly known as

  35. None
  36. @codeJENNerator Swagger • 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. • In earlier versions, it did not support hypermedia. Documenting across micro services was possible, but required a bit of setup. Depending on the library, some central information is duplicated.
  37. Body Slide - Dark Background All body text is Proxima

    Nova Regular • Subhead (18pt) • Level Two (18pt) • Level Three (18pt) • Level Four (18pt) Use the “Decrease/Increase Indent” 
 tools to change bullet levels Automation img src: https://flic.kr/p/eduUfU
  38. Body Slide - Dark Background All body text is Proxima

    Nova Regular • Subhead (18pt) • Level Two (18pt) • Level Three (18pt) • Level Four (18pt) Use the “Decrease/Increase Indent” 
 tools to change bullet levels img src: https://www.flickr.com/photos/ 24874528@N04/17125924230 SpringFox
  39. @codeJENNerator SpringFox • SpringFox: • Generates a Swagger Specification from

    source • Is very easy to setup (in simple cases) • No OpenAPI Spec 3.0 support!
  40. @codeJENNerator Custom Swagger Specification { "swagger": "2.0", "info": { "version":

    "1", "title": "My Service", "contact": { "name": "Company Name" }, "license": {} }, "host": "example.com", "basepath": "/docs" }
  41. @codeJENNerator Swagger UI

  42. @codeJENNerator URI Centric

  43. @codeJENNerator SpringFox UI approaches • Use SpringFox library • Copy

    static files and customize
  44. @codeJENNerator Considerations

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

  46. @codeJENNerator Customization • 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.
  47. @codeJENNerator Object Mapping img src: https://github.com/springfox/springfox/issues/281 img src: https://github.com/springfox/springfox/issues/281

  48. @codeJENNerator img src: https://github.com/springfox/springfox/issues/281 https://github.com/swagger-api/swagger-core/issues/97

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

    hypermedia support. • In OpenAPI Spec 3.0, there is some support for links
  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 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 } Annotation Hell
  52. @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 } Annotation Hell
  53. @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 } Annotation Hell X
  54. @codeJENNerator Annotation Hell 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 Swagger Advantages

  56. @codeJENNerator

  57. @codeJENNerator “Try it” Button Alternatives

  58. 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"}]
  59. None
  60. @codeJENNerator

  61. @codeJENNerator https://www.npmjs.com/package/ restdocs-to-postman

  62. @codeJENNerator Mix and Match

  63. Swagger2Markup https://github.com/Swagger2Markup/swagger2markup

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

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

  66. @codeJENNerator Spring Cloud Contract https://cloud.spring.io/spring-cloud-contract/

  67. @codeJENNerator Stubs

  68. @codeJENNerator Stubs

  69. @codeJENNerator What problem are you trying to solve?

  70. None
  71. Definitions Swagger (OpenAPI Specification) RAML

  72. Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis

  73. Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis Swagger

    UI Swagger2Markup
  74. Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc

    Markdown Wikis Swagger UI Swagger2Markup
  75. Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc

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

    Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First
  77. Spring REST Docs

  78. @codeJENNerator Green Red Refactor Test-Driven Development

  79. @codeJENNerator Test-Driven Documentation

  80. @codeJENNerator Red Test-Driven Documentation

  81. @codeJENNerator Red Test-Driven Documentation

  82. @codeJENNerator Document Red Test-Driven Documentation

  83. @codeJENNerator Document Red Test-Driven Documentation

  84. @codeJENNerator Document Green Red Test-Driven Documentation

  85. @codeJENNerator Document Green Red Test-Driven Documentation

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

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

  88. Winning Solution Test-Driven Documentation https://flic.kr/p/5XiKxU Winning Solution

  89. @codeJENNerator Winning Solution • Ensures documentation matches implementation • Encourages

    writing more tests • Reduces duplication in docs and tests • Removes annotations from source
  90. @codeJENNerator Spring REST Docs https://flic.kr/p/5XiKxU

  91. Game Changers https://flic.kr/p/9Tiv3U

  92. @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
  93. @codeJENNerator About Spring REST Docs projects.spring.io/spring-restdocs @springrestdocs https://github.com/spring-projects/spring-restdocs

  94. @codeJENNerator About Spring REST Docs projects.spring.io/spring-restdocs @springrestdocs https://github.com/spring-projects/spring-restdocs

  95. @codeJENNerator About Spring REST Docs •Start with reading the docs;

    The written docs are good! •Overview •Sponsored by Pivotal •Project Lead - Andy Wilkinson •Current Version - 2.0.1 released April 4 •Twitter Account and Official Logo
  96. @codeJENNerator About Spring REST Docs • Test-Driven Documentation with Spring

    REST Docs (Java and Spring Boot) - Spring I/O 2016 Andy Wilkinson • Writing comprehensive and guaranteed up-to-date REST API documentation - SpringOne Platform 2016 Anders Evers
  97. @codeJENNerator Out of the Box

  98. @codeJENNerator Out of the Box • Testing Frameworks • MockMVC

    • RestAssured • WebTestClient - NEW! • Build Tools • Gradle • Maven
  99. @codeJENNerator Out of the Box

  100. @codeJENNerator Out of the Box Documentation Format • AsciiDoc •

    Markdown Sample Projects • Spring Boot • Grails • Slate • TestNG • And more!
  101. @codeJENNerator Examples

  102. @codeJENNerator Groovier Spring REST Docs • Spring Boot • Ratpack

    • Grails
  103. @codeJENNerator Groovier Spring REST Docs Example - Spring Boot •

    Groovy Spring Boot Project • + Asciidoctor Gradle plugin • + Spring REST Docs WebTestClient to Spock tests • + Add to static assets during build and publish to GitHub pages
  104. @codeJENNerator Groovier Spring REST Docs

  105. @codeJENNerator Groovier Spring REST Docs

  106. @codeJENNerator Groovier Spring REST Docs

  107. @codeJENNerator Groovier Spring REST Docs Example - Spring Boot •

    Start with lazybones spring boot app • Add mock endpoints for example https://github.com/jlstrater/groovy-spring-boot- restdocs-example • Updated to Spring Boot 2.0.0.M7 https://www.callicoder.com/reactive-rest-apis-spring- webflux-reactive-mongo/
  108. @codeJENNerator

  109. @codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping()

    Mono<Greeting> createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux<Greeting> listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono<Greeting> getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } }
  110. @codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping()

    Mono<Greeting> createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux<Greeting> listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono<Greeting> getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting
  111. @codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping()

    Mono<Greeting> createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux<Greeting> listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono<Greeting> getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting List all greetings
  112. @codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping()

    Mono<Greeting> createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux<Greeting> listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono<Greeting> getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting List all greetings Get a greeting by id
  113. @codeJENNerator

  114. @codeJENNerator

  115. @codeJENNerator AsciiDoc • [introduction] • = Introduction • The Example

    API is a RESTful web service that shows how Spring REST docs works. • [[overview-http-verbs]] • == HTTP verbs • The Example API tries to adhere as closely as possible to standard HTTP and REST conventions in its • use of HTTP verbs. • |=== • | Verb | Usage • | `GET` • | Used to retrieve a resource
  116. @codeJENNerator Asciidoctor Gradle Plugin

  117. @codeJENNerator AsciiDoc Gradle Configuration apply plugin: 'org.asciidoctor.convert' asciidoctor { backends

    'html5' attributes 'source-highlighter' : 'prettify', 'imagesdir':'images', 'toc':'left', 'icons': 'font', 'setanchors':'true', 'idprefix':'', 'idseparator':'-', 'docinfo1':'true', }
  118. @codeJENNerator

  119. @codeJENNerator

  120. @codeJENNerator

  121. @codeJENNerator

  122. @codeJENNerator

  123. Project Reactor and the WebTestClient

  124. @codeJENNerator Setup @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification

    { @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .build() } }
  125. @codeJENNerator Setup @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification

    { @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .build() } } If context is null, remember to use spock-spring!!
  126. @codeJENNerator WebTestClient Call and Assertions this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": “Hello

    Warsaw JUG!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath(‘$.message').isEqualTo('Hello Warsaw JUG!’)
  127. @codeJENNerator

  128. @codeJENNerator

  129. @codeJENNerator Spring REST Docs Gradle Configuration dependencies { … testCompile

    "org.springframework.restdocs:spring-restdocs-webtestclient:${springRestDocsVersion}" asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}" } ext { snippetsDir = file('build/generated-snippets') } test { outputs.dir "$projectDir/src/main/resources/public" } asciidoctor { dependsOn test inputs.dir snippetsDir }
  130. @codeJENNerator Spring REST Docs with WebTestClient (setup) @CompileStatic @SpringBootTest(webEnvironment =

    SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation)) .build() }
  131. @codeJENNerator Spring REST Docs with WebTestClient (setup) @CompileStatic @SpringBootTest(webEnvironment =

    SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation)) .build() }
  132. @codeJENNerator Spring REST Docs with WebTestClient (setup) @CompileStatic @SpringBootTest(webEnvironment =

    SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation)) .build() }
  133. @codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and

    document creating a greeting with a custom name'() { expect: this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello Warsaw JUG!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello Warsaw JUG!') .consumeWith(document('greetings-post-example', preprocessRequest(prettyPrint()), requestFields( fieldWithPath('message').type(JsonFieldType.STRING) .description("The greeting's message"))))
  134. @codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and

    document creating a greeting with a custom name'() { expect: this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello Warsaw JUG!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello Warsaw JUG!') .consumeWith(document('greetings-post-example', preprocessRequest(prettyPrint()), requestFields( fieldWithPath('message').type(JsonFieldType.STRING) .description("The greeting's message"))))
  135. @codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and

    document get of a list of greetings'() { expect: this.webTestClient.get().uri('/').accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(document('greetings-list-example', preprocessResponse(prettyPrint()), responseFields(greetingList))) } FieldDescriptor[] greetingList = new FieldDescriptor().with { [fieldWithPath('[].id').type(JsonFieldType.STRING).optional() .description("The greeting's id"), fieldWithPath('[].message').type(JsonFieldType.STRING) .description("The greeting's message")]
  136. @codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and

    document get of a list of greetings'() { expect: this.webTestClient.get().uri('/').accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(document('greetings-list-example', preprocessResponse(prettyPrint()), responseFields(greetingList))) } FieldDescriptor[] greetingList = new FieldDescriptor().with { [fieldWithPath('[].id').type(JsonFieldType.STRING).optional() .description("The greeting's id"), fieldWithPath('[].message').type(JsonFieldType.STRING) .description("The greeting's message")]
  137. @codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and

    document get of a list of greetings'() { expect: this.webTestClient.get().uri('/').accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(document('greetings-list-example', preprocessResponse(prettyPrint()), responseFields(greetingList))) } FieldDescriptor[] greetingList = new FieldDescriptor().with { [fieldWithPath('[].id').type(JsonFieldType.STRING).optional() .description("The greeting's id"), fieldWithPath('[].message').type(JsonFieldType.STRING) .description("The greeting's message")]
  138. Error Messages

  139. Error Messages

  140. Error Messages

  141. Error Messages

  142. Error Messages

  143. @codeJENNerator

  144. @codeJENNerator

  145. 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 path-parameters.adoc request body request-parts.adoc response body
  146. @codeJENNerator http-request.adoc [source,http,options="nowrap"] ---- POST /greetings/ HTTP/1.1 Content-Type: application/json Accept:

    application/json Host: localhost:8080 Content-Length: 45 { "message" : "Hello Warsaw JUG!” } ----
  147. @codeJENNerator response-fields.adoc |=== |Path|Type|Description |`id` |`String` |The greeting's id |`message`

    |`String` |The greeting's message |===
  148. response-fields.adoc

  149. @codeJENNerator +

  150. @codeJENNerator +

  151. @codeJENNerator Adding The Snippets [[overview-errors]] == 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 delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[]
  152. @codeJENNerator Adding The Snippets [[overview-errors]] == 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 delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[]
  153. @codeJENNerator Adding The Snippets [[overview-errors]] == 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 delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[]
  154. @codeJENNerator Adding The Snippets [[overview-errors]] == 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 delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[]
  155. @codeJENNerator

  156. @codeJENNerator

  157. @codeJENNerator Building the docs • . src/docs/ asciidoc index.adoc build/asciidoc/

    html5 index.html .gradlew asciidoctor
  158. @codeJENNerator Building the docs • .

  159. @codeJENNerator Publishing Strategies

  160. @codeJENNerator Publishing Strategies • Hook in asciidoctor with the gradle

    build task • Run the asciidoctor test separately (but make sure to run AFTER the tests) • Send to static resources directory in the current app or send to a remote site (for example Github Pages
  161. @codeJENNerator

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

  163. @codeJENNerator 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/asciidoc/html5')) } }
  164. @codeJENNerator 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/asciidoc/html5')) } } If you use this method, remember to deploy docs at the same time as the project!
  165. @codeJENNerator http://jlstrater.github.io/groovy- spring-boot-restdocs-example

  166. @codeJENNerator http://jlstrater.github.io/groovy- spring-boot-restdocs-example ./gradlew publish

  167. @codeJENNerator ➜ build git:(master) restdocs-to-postman --input generated-snippets --export-format postman --determine-folder

    secondLastFolder --output postman-collection.json
  168. @codeJENNerator Special Use Case

  169. @codeJENNerator 
 +@WebMvcTest(controllers = GreetingsController)
 +@AutoConfigureRestDocs(
 + outputDir = "build/generated-snippets",


    + uriHost = “api.example.com”,
 + 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()
 // } }
  170. @codeJENNerator Groovier Spring REST Docs • Spring Boot • Ratpack

    • Grails
  171. @codeJENNerator Support for Rest- Assured

  172. @codeJENNerator

  173. @codeJENNerator

  174. @codeJENNerator

  175. @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
  176. @codeJENNerator Groovier Spring REST Docs Example - Ratpack 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
 }
 } }
  177. @codeJENNerator Groovier Spring REST Docs Example - Ratpack 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
  178. @codeJENNerator Groovier Spring REST Docs Example - Ratpack 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
  179. @codeJENNerator Groovier Spring REST Docs Example - Ratpack 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
  180. https://github.com/jayway/rest-assured

  181. @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()
 }
 }
  182. @codeJENNerator

  183. @codeJENNerator Documenting Public APIs

  184. @codeJENNerator Blog Post https://jennstrater.blogspot.com/2017/01/using-spring- rest-docs-to-document.html

  185. @codeJENNerator Outcomes

  186. @codeJENNerator Outcomes • Made it to production! :) • Team

    was happy with Spring REST Docs • Other dev teams like to see the examples
  187. @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(Spring Cloud Contracts), Jersey, AutoRestDocs, RAML
  188. @codeJENNerator Conclusion

  189. @codeJENNerator Conclusion • 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.
  190. @codeJENNerator Next Steps • Join #spring-restdocs on gitter • Join

    the Groovy Community on Slack - groovycommunity.com