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

test driven docs javaland 2019

test driven docs javaland 2019

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.
----
Jennifer 'Jenn' Strater is a Developer Advocate for Gradle. She is based out of Berlin, Germany.

1f28a0c1988421be3268026bd6bb6f49?s=128

jlstrater

March 18, 2019
Tweet

More Decks by jlstrater

Other Decks in Technology

Transcript

  1. @codeJENNerator March 20, 2019 Javaland Jenn Strater Documenting RESTful APIs

    with Spring REST Docs
  2. @codeJENNerator • 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 • Documenting RESTful APIs with Spring REST Docs (this

    talk): • English • Groovy • Gradle • REST-like APIs • focus: how to build documentation • Schnittstellen-Dokumentation mit Spring REST Docs with Gerrit Meier: • German • Java • Maven • Hypermedia • focus: live coding examples
  4. @codeJENNerator https://speakerdeck.com/jlstrater/test-driven-docs-javaland-2019 https://github.com/jlstrater/groovy-spring-boot-restdocs-example Follow Along

  5. @codeJENNerator • Open Source • Build Automation • Commercial SaaS

    Product (on-premises) • Development Productivity • for Gradle AND Apache Maven Gradle Products
  6. @codeJENNerator Before Gradle

  7. @codeJENNerator • Several years experience as a software engineer mostly

    with APIs • Spring REST Docs user and contributor since 2015 • Organizer of various Groovy community groups and events • GR8DI, GR8Conf, Groovy community slack Before Gradle
  8. @codeJENNerator Audience Background

  9. @codeJENNerator • Creating RESTful APIs • Spring Boot • JavaEE/Jakarta/

    Microprofile • Ratpack Audience Background
  10. @codeJENNerator • Creating RESTful APIs • Spring Boot • JavaEE/Jakarta/

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

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

  13. ✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦

    Spring REST Docs
  14. ✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦

    Spring REST Docs
  15. Case Study

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

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

  18. @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 REST Maturity Model
  19. @codeJENNerator Attribution: @Alvaro_Sanchez

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

  22. @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. Keep it DRY. Central Information
  23. @codeJENNerator Document Design

  24. @codeJENNerator

  25. @codeJENNerator • 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! Document Design
  26. @codeJENNerator

  27. @codeJENNerator • 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 other information too! Document Design
  28. @codeJENNerator Available Tools

  29. @codeJENNerator

  30. @codeJENNerator Definitions Swagger (OpenAPI Specification) RAML

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

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

    Swagger UI Swagger2Markup
  33. @codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation

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

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

    AsciiDoc Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First
  36. @codeJENNerator Definitions Swagger (OpenAPI Specification) RAML

  37. @codeJENNerator

  38. @codeJENNerator

  39. @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. • 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. Swagger
  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 Automation img src: https://flic.kr/p/eduUfU

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

  43. @codeJENNerator • Generates a Swagger Specification from source • Is

    very easy to setup (in simple cases) • No OpenAPI Spec 3.0 support! SpringFox
  44. @codeJENNerator Swagger UI

  45. @codeJENNerator

  46. @codeJENNerator • Use SpringFox library • Copy static files and

    customize SpringFox UI approaches
  47. ✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦

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

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

  51. @codeJENNerator https://github.com/swagger-api/swagger-core/issues/97

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

    support. • In OpenAPI Spec 3.0, there is some support for links Hypermedia
  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 }
  54. @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
  55. @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
  56. @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
  57. @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 }
  58. @codeJENNerator Swagger Advantages

  59. @codeJENNerator

  60. @codeJENNerator “Try it” Button Alternatives

  61. @codeJENNerator

  62. @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"}]
  63. @codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation

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

  65. @codeJENNerator • What problem are you trying to solve? •

    What kind of documentation do you need to solve that problem? Summary
  66. ✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦

    Spring REST Docs
  67. @codeJENNerator AsciiDoc = Example API Guide Jenn Strater; :doctype: book

    :icons: font :source-highlighter: highlightjs :toc: left :toclevels: 4 :sectlinks: [introduction] = Introduction The Example API is a RESTful web service that shows how Spring REST docs works. Interact with these endpoints in https://documenter.getpostman.com/view/6964523/ S17nVWHG#627c80fe-7520-1ff0-8f73-9b989042bd3e[image:postman-logo.png[Postman, title="Postman", width=100]]. [[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.
  68. @codeJENNerator Asciidoctor Gradle Plugin https://asciidoctor.org/docs/asciidoctor-gradle-plugin/

  69. @codeJENNerator AsciiDoc Gradle Configuration plugins { id "org.asciidoctor.convert" version "1.5.3"

    } asciidoctor { backends 'html5' attributes 'source-highlighter' : 'prettify', 'imagesdir':'images', 'toc':'left', 'icons': 'font', 'setanchors':'true', 'idprefix':'', 'idseparator':'-', 'docinfo1':'true', }
  70. @codeJENNerator Building the docs src/docs/ asciidoc index.adoc build/asciidoc/ html5 index.html

    ./gradlew asciidoctor
  71. @codeJENNerator

  72. @codeJENNerator • Docs-as-Code, arc42, AsciiDoc, Gradle & Co. im Einsatz

    by Ralf Müller (yesterday & April 8 at Dortmund JUG). • Test-Driven Approaches to Documentation by Jenn Strater April 2 at MicroXchg in Berlin. • Asciidoctor: Because Writing Docs Does Not Have to Suck by Andres Almiray April 4 at K15t TeamTalks in Stuttgart. More on Asciidoc
  73. ✦ Approaches to Documentation ✦ Considerations ✦ Building Documentation ✦

    Spring REST Docs
  74. @codeJENNerator Green Red Refactor Test-Driven Development

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

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

  77. @codeJENNerator • Ensures documentation matches implementation • Encourages writing more

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

  79. @codeJENNerator •Start with reading the docs; The written docs are

    good! •Overview •Sponsored by Pivotal •Project Lead - Andy Wilkinson •Current Version - 2.0.0 released Nov 28 •Twitter Account and Official Logo About Spring REST Docs
  80. @codeJENNerator • 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 History of Spring REST Docs
  81. @codeJENNerator projects.spring.io/spring-restdocs github.com/spring-projects/spring-restdocs gitter.im/spring-projects/spring-restdocs @springrestdocs About Spring REST Docs

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

  83. @codeJENNerator •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 Game Changers
  84. @codeJENNerator Out of the Box

  85. @codeJENNerator • Testing Frameworks • MockMVC • RestAssured • WebTestClient

    • Build Tools • Gradle • Maven Out of the Box
  86. @codeJENNerator Out of the Box

  87. @codeJENNerator Documentation Format • AsciiDoc • Markdown Sample Projects •

    Spring Boot • Grails • Slate • TestNG • And more! Out of the Box
  88. Example

  89. @codeJENNerator • 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 Spring REST Docs Example
  90. @codeJENNerator Groovy Spring REST Docs

  91. @codeJENNerator

  92. @codeJENNerator Project Reactor and the WebTestClient

  93. @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) } }
  94. @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
  95. @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
  96. @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
  97. @codeJENNerator @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() } } Setup
  98. @codeJENNerator @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() } } Setup If context is null, remember to use spock-spring!!
  99. @codeJENNerator WebTestClient Call and Assertions void 'test and document creating

    a greeting with a custom name'() { when: def result = this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello!') JsonSlurper slurper = new JsonSlurper() greetingId = slurper.parseText(new String(result.returnResult().body)).id then: assert greetingId }
  100. @codeJENNerator

  101. @codeJENNerator Spring REST Docs Gradle Configuration dependencies { asciidoctor “org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}"

    testCompile "org.springframework.restdocs:spring-restdocs-webtestclient:${springRestDocsVersion}" } ext { snippetsDir = "$buildDir/generated-snippets" } test { outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir dependsOn test } build { dependsOn asciidoctor }
  102. @codeJENNerator Spring REST Docs Gradle Configuration dependencies { asciidoctor “org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}"

    testCompile "org.springframework.restdocs:spring-restdocs-webtestclient:${springRestDocsVersion}" } ext { snippetsDir = "$buildDir/generated-snippets" } test { outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir dependsOn test } build { dependsOn asciidoctor } Or mockmvc or restassured
  103. @codeJENNerator @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) .operationPreprocessors() .withRequestDefaults(prettyPrint()) .withResponseDefaults(prettyPrint())) .build() } } Spring REST Docs with WebTestClient (setup)
  104. @codeJENNerator @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) .operationPreprocessors() .withRequestDefaults(prettyPrint()) .withResponseDefaults(prettyPrint())) .build() } } Spring REST Docs with WebTestClient (setup)
  105. @codeJENNerator @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) .operationPreprocessors() .withRequestDefaults(prettyPrint()) .withResponseDefaults(prettyPrint())) .build() } } Spring REST Docs with WebTestClient (setup)
  106. @codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and

    document creating a greeting with a custom name'() { when: def result = this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello!') .consumeWith(document('greetings-post-example', requestFields( fieldWithPath('message') .type(JsonFieldType.STRING) .description("The greeting's message")))) JsonSlurper slurper = new JsonSlurper() greetingId = slurper.parseText(new String(result.returnResult().body)).id then: assert greetingId }
  107. @codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and

    document creating a greeting with a custom name'() { when: def result = this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello!') .consumeWith(document('greetings-post-example', requestFields( fieldWithPath('message') .type(JsonFieldType.STRING) .description("The greeting's message")))) JsonSlurper slurper = new JsonSlurper() greetingId = slurper.parseText(new String(result.returnResult().body)).id then: assert greetingId }
  108. @codeJENNerator

  109. @codeJENNerator

  110. @codeJENNerator

  111. @codeJENNerator

  112. @codeJENNerator

  113. @codeJENNerator

  114. @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 path-parameters.adoc request body request-parts.adoc response body
  115. @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!" } ----
  116. @codeJENNerator response-fields.adoc |=== |Path|Type|Description |`id` |`String` |The greeting's id |`message`

    |`String` |The greeting's message |===
  117. @codeJENNerator +

  118. @codeJENNerator [[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[] Adding The Snippets
  119. @codeJENNerator [[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[] Adding The Snippets
  120. @codeJENNerator [[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[] Adding The Snippets
  121. @codeJENNerator [[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[] Adding The Snippets
  122. @codeJENNerator

  123. @codeJENNerator

  124. @codeJENNerator

  125. Publishing Strategies

  126. @codeJENNerator • 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 Publishing Strategies
  127. @codeJENNerator

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

  129. @codeJENNerator Publish to GitHub Pages In build.gradle: plugins { id

    'org.ajoberstar.git-publish' version '2.0.0' } In publish.gradle: gitPublish { repoUri = 'git@github.com:jlstrater/groovy-spring-boot-restdocs-example.git' branch = 'gh-pages' contents { from file('build/asciidoc/html5') } commitMessage = 'Updating the doc example' }
  130. @codeJENNerator Publish to GitHub Pages In build.gradle: plugins { id

    'org.ajoberstar.git-publish' version '2.0.0' } In publish.gradle: gitPublish { repoUri = 'git@github.com:jlstrater/groovy-spring-boot-restdocs-example.git' branch = 'gh-pages' contents { from file('build/asciidoc/html5') } commitMessage = 'Updating the doc example' } If you use this method, remember to deploy docs at the same time as the project!
  131. @codeJENNerator

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

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

  134. @codeJENNerator Special Use Case

  135. @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()
 // } }
  136. @codeJENNerator Support for Rest-Assured

  137. @codeJENNerator

  138. @codeJENNerator

  139. @codeJENNerator • Ratpack Example Project • https://github.com/ratpack/example-books • Spring RESTdocs

    RestAssured • https://github.com/ratpack/example-books/pull/25 Groovier Spring REST Docs Example - Ratpack
  140. @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
 }
 } } Groovier Spring REST Docs Example - Ratpack
  141. @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
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842
  142. @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
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842 Get all books /api/books
  143. @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
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842 Get all books /api/books Post to create a new book /api/books
  144. @codeJENNerator https://github.com/jayway/rest-assured

  145. @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()
 }
 }
  146. @codeJENNerator

  147. @codeJENNerator Documenting Public APIs

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

  149. @codeJENNerator Related Tools & Libraries

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

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

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

  153. @codeJENNerator Spring Cloud Contract

  154. @codeJENNerator Stubs

  155. @codeJENNerator https://github.com/fbenz/restdocs-to-postman

  156. @codeJENNerator

  157. @codeJENNerator Spring REST Docs API specification Integration or or

  158. @codeJENNerator • Adding Security and Headers • Documenting Constraints •

    Hypermedia Support • XML Support • Using Markdown instead of Asciidoc • Third Party Extensions for WireMock, Jersey, Spring Cloud Contracts, AutoRestDocs Read the Docs for More On…
  159. @codeJENNerator Outcomes

  160. @codeJENNerator • Made it to production! (in 2016) • At

    the time, the team was happy with Spring REST Docs • Other dev teams like to see the examples Outcomes
  161. @codeJENNerator Next Steps

  162. @codeJENNerator Join #spring-restdocs on gitter https://gitter.im/spring- projects/spring-restdocs Join the Groovy

    Community on Slack groovycommunity.com Slides: https://speakerdeck.com/jlstrater/test-driven-docs- javaland-2019 Example Project: https://github.com/jlstrater/groovy- spring-boot-restdocs-example
  163. Conclusion

  164. @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. Conclusion
  165. w Thank You @codeJENNerator