$30 off During Our Annual Pro Sale. View Details »

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.

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

    View Slide

  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.

    View Slide

  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

    View Slide

  4. @codeJENNerator
    https://speakerdeck.com/jlstrater/test-driven-docs-javaland-2019
    https://github.com/jlstrater/groovy-spring-boot-restdocs-example
    Follow Along

    View Slide

  5. @codeJENNerator
    • Open Source
    • Build Automation
    • Commercial SaaS Product (on-premises)
    • Development Productivity
    • for Gradle AND Apache Maven
    Gradle Products

    View Slide

  6. @codeJENNerator
    Before Gradle

    View Slide

  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

    View Slide

  8. @codeJENNerator
    Audience Background

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. Case Study

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  19. @codeJENNerator
    Attribution: @Alvaro_Sanchez

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  23. @codeJENNerator
    Document Design

    View Slide

  24. @codeJENNerator

    View Slide

  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

    View Slide

  26. @codeJENNerator

    View Slide

  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

    View Slide

  28. @codeJENNerator
    Available Tools

    View Slide

  29. @codeJENNerator

    View Slide

  30. @codeJENNerator
    Definitions
    Swagger (OpenAPI Specification)
    RAML

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. @codeJENNerator
    Definitions
    Swagger (OpenAPI Specification)
    RAML

    View Slide

  37. @codeJENNerator

    View Slide

  38. @codeJENNerator

    View Slide

  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

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

    View Slide

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

    View Slide

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

    View Slide

  44. @codeJENNerator
    Swagger UI

    View Slide

  45. @codeJENNerator

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    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 }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 }

    View Slide

  58. @codeJENNerator
    Swagger Advantages

    View Slide

  59. @codeJENNerator

    View Slide

  60. @codeJENNerator
    “Try it” Button Alternatives

    View Slide

  61. @codeJENNerator

    View Slide

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

    View Slide

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

    View Slide

  64. @codeJENNerator
    Spring
    REST
    Docs

    View Slide

  65. @codeJENNerator
    • What problem are you trying to solve?
    • What kind of documentation do you need to solve that
    problem?
    Summary

    View Slide

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

    View Slide

  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.

    View Slide

  68. @codeJENNerator
    Asciidoctor Gradle Plugin
    https://asciidoctor.org/docs/asciidoctor-gradle-plugin/

    View Slide

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

    View Slide

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

    View Slide

  71. @codeJENNerator

    View Slide

  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

    View Slide

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

    View Slide

  74. @codeJENNerator
    Green
    Red Refactor
    Test-Driven Development

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  81. @codeJENNerator
    projects.spring.io/spring-restdocs
    github.com/spring-projects/spring-restdocs
    gitter.im/spring-projects/spring-restdocs
    @springrestdocs
    About Spring REST Docs

    View Slide

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

    View Slide

  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

    View Slide

  84. @codeJENNerator
    Out of the Box

    View Slide

  85. @codeJENNerator
    • Testing Frameworks
    • MockMVC
    • RestAssured
    • WebTestClient
    • Build Tools
    • Gradle
    • Maven
    Out of the Box

    View Slide

  86. @codeJENNerator
    Out of the Box

    View Slide

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

    View Slide

  88. Example

    View Slide

  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

    View Slide

  90. @codeJENNerator
    Groovy Spring REST Docs

    View Slide

  91. @codeJENNerator

    View Slide

  92. @codeJENNerator
    Project Reactor and the
    WebTestClient

    View Slide

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

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

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

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

  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

    View Slide

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

    View Slide

  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
    }

    View Slide

  100. @codeJENNerator

    View Slide

  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
    }

    View Slide

  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

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  108. @codeJENNerator

    View Slide

  109. @codeJENNerator

    View Slide

  110. @codeJENNerator

    View Slide

  111. @codeJENNerator

    View Slide

  112. @codeJENNerator

    View Slide

  113. @codeJENNerator

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  117. @codeJENNerator
    +

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  122. @codeJENNerator

    View Slide

  123. @codeJENNerator

    View Slide

  124. @codeJENNerator

    View Slide

  125. Publishing Strategies

    View Slide

  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

    View Slide

  127. @codeJENNerator

    View Slide

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

    View Slide

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

    View Slide

  130. @codeJENNerator
    Publish to GitHub Pages
    In build.gradle:
    plugins {
    id 'org.ajoberstar.git-publish' version '2.0.0'
    }
    In publish.gradle:
    gitPublish {
    repoUri = 'g[email protected]: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!

    View Slide

  131. @codeJENNerator

    View Slide

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

    View Slide

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

    View Slide

  134. @codeJENNerator
    Special Use Case

    View Slide

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

    // }
    }

    View Slide

  136. @codeJENNerator
    Support for Rest-Assured

    View Slide

  137. @codeJENNerator

    View Slide

  138. @codeJENNerator

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    }

    }

    View Slide

  146. @codeJENNerator

    View Slide

  147. @codeJENNerator
    Documenting Public APIs

    View Slide

  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

    View Slide

  149. @codeJENNerator
    Related Tools & Libraries

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  153. @codeJENNerator
    Spring Cloud Contract

    View Slide

  154. @codeJENNerator
    Stubs

    View Slide

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

    View Slide

  156. @codeJENNerator

    View Slide

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

    View Slide

  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…

    View Slide

  159. @codeJENNerator
    Outcomes

    View Slide

  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

    View Slide

  161. @codeJENNerator
    Next Steps

    View Slide

  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

    View Slide

  163. Conclusion

    View Slide

  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

    View Slide

  165. w
    Thank You
    @codeJENNerator

    View Slide