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.

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

    View Slide

  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.

    View Slide

  3. @codeJENNerator
    Outline
    • API Documentation
    Background
    • Approaches to
    Documentation
    • Considerations

    • Test-Driven
    Documentation
    • Spring REST Docs
    • Examples

    View Slide

  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

    View Slide

  5. @codeJENNerator
    About Me

    View Slide

  6. @codeJENNerator

    View Slide

  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.

    View Slide

  8. @codeJENNerator
    Audience Background
    • Creating RESTful APIs
    • Spring Boot
    • Grails
    • Ratpack
    • API Documentation
    • Wiki Pages, Word
    Documents,
    Confluence, etc
    • Asciidoc / Asciidoctor
    • Swagger / RAML

    View Slide

  9. @codeJENNerator
    src: https://flic.kr/p/rehEf5

    View Slide

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

    View Slide

  11. @codeJENNerator
    Case Study

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  15. Documenting RESTfullike
    APIs with
    Spring REST Docs
    By Jenn Strater
    @codeJENNerator

    View Slide

  16. @codeJENNerator
    REST Maturity Model
    Attribution: @Alvaro_Sanchez

    View Slide

  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

    View Slide

  18. @codeJENNerator
    Central Information
    Security
    Http Verbs
    Error
    Handling
    Http Status

    View Slide

  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.

    View Slide

  20. @codeJENNerator
    Document Design

    View Slide

  21. @codeJENNerator
    URI Centric

    View Slide

  22. @codeJENNerator
    URI Centric

    View Slide

  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!

    View Slide

  24. @codeJENNerator
    Available Tools

    View Slide

  25. View Slide

  26. Definitions
    Swagger (OpenAPI Specification)
    RAML

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  32. Definitions
    Swagger (OpenAPI Specification)
    RAML

    View Slide

  33. View Slide

  34. The specification formerly known as

    View Slide

  35. View Slide

  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.

    View Slide

  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

    View Slide

  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

    View Slide

  39. @codeJENNerator
    SpringFox
    • SpringFox:
    • Generates a Swagger Specification from source
    • Is very easy to setup (in simple cases)
    • No OpenAPI Spec 3.0 support!

    View Slide

  40. @codeJENNerator
    Custom Swagger Specification
    {
    "swagger": "2.0",
    "info": {
    "version": "1",
    "title": "My Service",
    "contact": {
    "name": "Company Name"
    },
    "license": {}
    },
    "host": "example.com",
    "basepath": "/docs"
    }

    View Slide

  41. @codeJENNerator
    Swagger UI

    View Slide

  42. @codeJENNerator
    URI Centric

    View Slide

  43. @codeJENNerator
    SpringFox UI approaches
    • Use SpringFox library
    • Copy static files and customize

    View Slide

  44. @codeJENNerator
    Considerations

    View Slide

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

    View Slide

  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.

    View Slide

  47. @codeJENNerator
    Object Mapping
    img src: https://github.com/springfox/springfox/issues/281
    img src: https://github.com/springfox/springfox/issues/281

    View Slide

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

    View Slide

  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

    View Slide

  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 }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 }

    View Slide

  55. @codeJENNerator
    Swagger Advantages

    View Slide

  56. @codeJENNerator

    View Slide

  57. @codeJENNerator
    “Try it” Button
    Alternatives

    View Slide

  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"}]

    View Slide

  59. View Slide

  60. @codeJENNerator

    View Slide

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

    View Slide

  62. @codeJENNerator
    Mix and Match

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  67. @codeJENNerator
    Stubs

    View Slide

  68. @codeJENNerator
    Stubs

    View Slide

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

    View Slide

  70. View Slide

  71. Definitions
    Swagger (OpenAPI Specification)
    RAML

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  77. Spring
    REST
    Docs

    View Slide

  78. @codeJENNerator
    Green
    Red Refactor
    Test-Driven Development

    View Slide

  79. @codeJENNerator
    Test-Driven Documentation

    View Slide

  80. @codeJENNerator
    Red
    Test-Driven Documentation

    View Slide

  81. @codeJENNerator
    Red
    Test-Driven Documentation

    View Slide

  82. @codeJENNerator
    Document
    Red
    Test-Driven Documentation

    View Slide

  83. @codeJENNerator
    Document
    Red
    Test-Driven Documentation

    View Slide

  84. @codeJENNerator
    Document Green
    Red
    Test-Driven Documentation

    View Slide

  85. @codeJENNerator
    Document Green
    Red
    Test-Driven Documentation

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  89. @codeJENNerator
    Winning Solution
    • Ensures documentation matches implementation
    • Encourages writing more tests
    • Reduces duplication in docs and tests
    • Removes annotations from source

    View Slide

  90. @codeJENNerator
    Spring REST Docs
    https://flic.kr/p/5XiKxU

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  97. @codeJENNerator
    Out of the Box

    View Slide

  98. @codeJENNerator
    Out of the Box
    • Testing Frameworks
    • MockMVC
    • RestAssured
    • WebTestClient - NEW!
    • Build Tools
    • Gradle
    • Maven

    View Slide

  99. @codeJENNerator
    Out of the Box

    View Slide

  100. @codeJENNerator
    Out of the Box
    Documentation Format
    • AsciiDoc
    • Markdown
    Sample Projects
    • Spring Boot
    • Grails
    • Slate
    • TestNG
    • And more!

    View Slide

  101. @codeJENNerator
    Examples

    View Slide

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

    View Slide

  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

    View Slide

  104. @codeJENNerator
    Groovier Spring REST Docs

    View Slide

  105. @codeJENNerator
    Groovier Spring REST Docs

    View Slide

  106. @codeJENNerator
    Groovier Spring REST Docs

    View Slide

  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/

    View Slide

  108. @codeJENNerator

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  113. @codeJENNerator

    View Slide

  114. @codeJENNerator

    View Slide

  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

    View Slide

  116. @codeJENNerator
    Asciidoctor Gradle Plugin

    View Slide

  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',
    }

    View Slide

  118. @codeJENNerator

    View Slide

  119. @codeJENNerator

    View Slide

  120. @codeJENNerator

    View Slide

  121. @codeJENNerator

    View Slide

  122. @codeJENNerator

    View Slide

  123. Project Reactor and
    the WebTestClient

    View Slide

  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()
    }
    }

    View Slide

  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!!

    View Slide

  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!’)

    View Slide

  127. @codeJENNerator

    View Slide

  128. @codeJENNerator

    View Slide

  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
    }

    View Slide

  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()
    }

    View Slide

  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()
    }

    View Slide

  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()
    }

    View Slide

  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"))))

    View Slide

  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"))))

    View Slide

  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")]

    View Slide

  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")]

    View Slide

  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")]

    View Slide

  138. Error Messages

    View Slide

  139. Error Messages

    View Slide

  140. Error Messages

    View Slide

  141. Error Messages

    View Slide

  142. Error Messages

    View Slide

  143. @codeJENNerator

    View Slide

  144. @codeJENNerator

    View Slide

  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

    View Slide

  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!”
    }
    ----

    View Slide

  147. @codeJENNerator
    response-fields.adoc
    |===
    |Path|Type|Description
    |`id`
    |`String`
    |The greeting's id
    |`message`
    |`String`
    |The greeting's message
    |===

    View Slide

  148. response-fields.adoc

    View Slide

  149. @codeJENNerator
    +

    View Slide

  150. @codeJENNerator
    +

    View Slide

  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[]

    View Slide

  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[]

    View Slide

  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[]

    View Slide

  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[]

    View Slide

  155. @codeJENNerator

    View Slide

  156. @codeJENNerator

    View Slide

  157. @codeJENNerator
    Building the docs
    • .
    src/docs/
    asciidoc
    index.adoc
    build/asciidoc/
    html5
    index.html
    .gradlew asciidoctor

    View Slide

  158. @codeJENNerator
    Building the docs
    • .

    View Slide

  159. @codeJENNerator
    Publishing Strategies

    View Slide

  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

    View Slide

  161. @codeJENNerator

    View Slide

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

    View Slide

  163. @codeJENNerator
    publish.gradle
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'org.ajoberstar:gradle-git:1.1.0'
    }
    }
    apply plugin: 'org.ajoberstar.github-pages'
    githubPages {
    repoUri = 'g[email protected]:jlstrater/groovy-spring-boot-restdocs-example.git'
    pages {
    from(file('build/asciidoc/html5'))
    }
    }

    View Slide

  164. @codeJENNerator
    publish.gradle
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'org.ajoberstar:gradle-git:1.1.0'
    }
    }
    apply plugin: 'org.ajoberstar.github-pages'
    githubPages {
    repoUri = 'g[email protected]: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!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  168. @codeJENNerator
    Special Use Case

    View Slide

  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()

    // }
    }

    View Slide

  170. @codeJENNerator
    Groovier Spring REST Docs
    • Spring Boot
    • Ratpack
    • Grails

    View Slide

  171. @codeJENNerator
    Support for Rest-
    Assured

    View Slide

  172. @codeJENNerator

    View Slide

  173. @codeJENNerator

    View Slide

  174. @codeJENNerator

    View Slide

  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

    View Slide

  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 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

    }

    }
    }

    View Slide

  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 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

    View Slide

  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 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

    View Slide

  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 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

    View Slide

  180. https://github.com/jayway/rest-assured

    View Slide

  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()

    }

    }

    View Slide

  182. @codeJENNerator

    View Slide

  183. @codeJENNerator
    Documenting Public
    APIs

    View Slide

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

    View Slide

  185. @codeJENNerator
    Outcomes

    View Slide

  186. @codeJENNerator
    Outcomes
    • Made it to production! :)
    • Team was happy with Spring REST Docs
    • Other dev teams like to see the examples

    View Slide

  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

    View Slide

  188. @codeJENNerator
    Conclusion

    View Slide

  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.

    View Slide

  190. @codeJENNerator
    Next Steps
    • Join #spring-restdocs on gitter
    • Join the Groovy Community on Slack -
    groovycommunity.com

    View Slide