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

Test Driven Docs APIConf DE 2017

jlstrater
September 20, 2017

Test Driven Docs APIConf DE 2017

As presented at API Conference DE 2017 on September 20, 2017
------------

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. 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. Examples will be in Spring Boot and Groovy, but the concepts are applicable to other ecosystems too.

jlstrater

September 20, 2017
Tweet

More Decks by jlstrater

Other Decks in Technology

Transcript

  1. A Test-Driven Approach to
    Documenting RESTful APIs
    Jenn Strater
    @codeJENNerator

    View Slide

  2. @codeJENNerator
    Note For Those Viewing Slides Online
    • Bulleted text like this indicates the key points mentioned on a previous slide.
    They may not have been included in the official presentation.
    • If this view does not support links, the links will work in the pdf. In speakerdeck,
    you can click the ‘download pdf’ button at the right.

    View Slide

  3. @codeJENNerator
    • Background of API
    Documentation
    • Approaches
    • Considerations
    • Test-Driven
    Documentation
    • Spring REST Docs
    • Examples
    Outline

    View Slide

  4. @codeJENNerator
    https://speakerdeck.com/jlstrater/test-driven-docs-apiconf-de-2017
    https://github.com/jlstrater/groovy-spring-boot-restdocs-example
    https://github.com/ratpack/example-books
    https://github.com/jlstrater/spring-restdocs-public-api-example
    Follow Along

    View Slide

  5. @codeJENNerator

    View Slide

  6. @codeJENNerator
    About Me
    - Senior Engineer at Zenjob as of June 2017
    - Spent the last 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
    since then is the subject of this talk.
    - Co-founder of Gr8Ladies and talk about women in the Groovy Community all over the world
    - Passionate about bring new people into the Groovy community through free introductory workshops called
    Gr8Workshops.

    View Slide

  7. @codeJENNerator
    Background

    View Slide

  8. @codeJENNerator
    Background
    Creating RESTful APIs
    • Spring Boot
    • Grails
    • Ratpack
    • Other MVC-like frameworks?

    View Slide

  9. @codeJENNerator
    Background
    Creating RESTful APIs
    • Spring Boot
    • Grails
    • Ratpack
    • Other MVC-like frameworks?
    API Documentation
    • Wiki Pages, Word Docs,
    Confluence, etc
    • Swagger/RAML
    • Asciidoctor

    View Slide

  10. @codeJENNerator
    img src: https://flic.kr/p/rehEf5

    View Slide

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

    View Slide

  12. @codeJENNerator
    Example Case Study

    View Slide

  13. @codeJENNerator
    Factors in Choosing a
    Documentation Solution

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. @codeJENNerator

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. @codeJENNerator
    Design

    View Slide

  22. @codeJENNerator
    Design
    VS

    View Slide

  23. @codeJENNerator
    • On the left, is a swagger ui example
    • On right, is an example from Spring Rest Docs using
    Asciidoc.
    Notice the very different way of organizing information.
    URIs are difficult to read whereas resource document
    design organizes information by topic and includes urls in
    the examples only.

    View Slide

  24. @codeJENNerator

    View Slide

  25. @codeJENNerator
    Definitions
    Swagger (OpenAPI)
    RAML

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. @codeJENNerator
    Definitions

    View Slide

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

    View Slide

  33. @codeJENNerator
    Swagger Approaches for
    the JVM

    View Slide

  34. @codeJENNerator
    Automation
    img src: https://flic.kr/p/eduUfU

    View Slide

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

    View Slide

  36. @codeJENNerator
    SpringFox:
    • Generates a Swagger Specification from source
    • Is very easy to setup (in simple cases)

    View Slide

  37. @codeJENNerator
    Installation
    • io.springfox:springfox-swagger2
    • Some simple config in application main

    View Slide

  38. @codeJENNerator
    Custom Swagger
    Specification

    View Slide

  39. @codeJENNerator
    {

    "swagger": "2.0",

    "info": {

    "version": "1",

    "title": "My Service",

    "contact": {

    "name": "Company Name"

    },

    "license": {}

    },

    "host": "example.com",

    "basepath": "/docs",

    "tags": [

    {

    "name": "group name",

    "description": "what this group of api endpoints is for",

    }

    ],

    "paths": {

    "/api/v1/groupname/resource":

    .

    .

    .

    }

    View Slide

  40. @codeJENNerator

    View Slide

  41. @codeJENNerator

    View Slide

  42. @codeJENNerator
    • Swagger UI Approaches
    • Use SpringFox library
    • Copy static files and customize

    View Slide

  43. @codeJENNerator
    Considerations

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  47. @codeJENNerator
    • Along the same lines, the generated Swagger Spec doesn’t
    fully define complex and nested objects.
    • We ended up writing a lot of these object mappers.

    View Slide

  48. @codeJENNerator
    Hypermedia Support

    View Slide

  49. @codeJENNerator
    • In Swagger/OpenAPI Spec 2.0, there was no hypermedia
    support.
    • In OpenAPI Spec 3.0

    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
    Annotation Hell
    1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)

    2 @RequestMapping(value = '/v1/serviceName/actionName', method =
    3 RequestMethod.POST)

    4 @ApiOperation(value = '/actionName',

    5 notes = 'Enables or disables setting via "1" or "0", respectively')

    6 @ApiResponses(value = [

    7 @ApiResponse(code = 200, response = CustomSettingResponse, message =
    8 ‘Successful setting update'),

    9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid
    10 user input'),

    11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected
    12 server error')

    13 ])

    14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {

    15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (

    16 settingsValue.fieldOne,

    17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE,
    18 new Double(settingsValue.value))]

    19 )

    20 api.saveUpdatedSetting(request)

    21 }

    View Slide

  52. @codeJENNerator
    Annotation Hell
    1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)

    2 @RequestMapping(value = '/v1/serviceName/actionName', method =
    3 RequestMethod.POST)

    4 @ApiOperation(value = '/actionName',

    5 notes = 'Enables or disables setting via "1" or "0", respectively')

    6 @ApiResponses(value = [

    7 @ApiResponse(code = 200, response = CustomSettingResponse, message =
    8 ‘Successful setting update'),

    9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid
    10 user input'),

    11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected
    12 server error')

    13 ])

    14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {

    15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (

    16 settingsValue.fieldOne,

    17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE,
    18 new Double(settingsValue.value))]

    19 )

    20 api.saveUpdatedSetting(request)

    21 }

    View Slide

  53. @codeJENNerator
    Annotation Hell
    1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)

    2 @RequestMapping(value = '/v1/serviceName/actionName', method =
    3 RequestMethod.POST)

    4 @ApiOperation(value = '/actionName',

    5 notes = 'Enables or disables setting via "1" or "0", respectively')

    6 @ApiResponses(value = [

    7 @ApiResponse(code = 200, response = CustomSettingResponse, message =
    8 ‘Successful setting update'),

    9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid
    10 user input'),

    11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected
    12 server error')

    13 ])

    14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {

    15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (

    16 settingsValue.fieldOne,

    17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE,
    18 new Double(settingsValue.value))]

    19 )

    20 api.saveUpdatedSetting(request)

    21 }
    X

    View Slide

  54. @codeJENNerator
    1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)

    2 @RequestMapping(value = '/v1/serviceName/actionName', method =
    3 RequestMethod.POST)
    4 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {

    5 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (

    6 settingsValue.fieldOne,

    7 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE,
    8 new Double(settingsValue.value))]

    9 )

    10 api.saveUpdatedSetting(request)

    11 }

    View Slide

  55. @codeJENNerator
    Advantages

    View Slide

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

    View Slide

  57. @codeJENNerator
    Swagger UI “Try-It”
    Alternatives

    View Slide

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

    View Slide

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

  60. @codeJENNerator
    Mixing Spring REST
    Docs and Swagger

    View Slide

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

    View Slide

  62. @codeJENNerator

    View Slide

  63. @codeJENNerator

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  68. @codeJENNerator
    Test-Driven Documentation
    Green
    Red Refactor

    View Slide

  69. @codeJENNerator
    Test-Driven Documentation

    View Slide

  70. @codeJENNerator
    Test-Driven Documentation
    Red

    View Slide

  71. @codeJENNerator
    Test-Driven Documentation
    Red

    View Slide

  72. @codeJENNerator
    Test-Driven Documentation
    Document
    Red

    View Slide

  73. @codeJENNerator
    Test-Driven Documentation
    Document
    Red

    View Slide

  74. @codeJENNerator
    Test-Driven Documentation
    Document Green
    Red

    View Slide

  75. @codeJENNerator
    Test-Driven Documentation
    Document Green
    Red

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  79. @codeJENNerator
    Winning Solution
    - Ensures documentation matches implementation

    - Encourages writing more tests

    - Reduces duplication in docs and tests

    - Removes annotations from source

    View Slide

  80. @codeJENNerator
    Spring REST Docs

    View Slide

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

    View Slide

  82. @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 autogenerated Swagger libraries

    View Slide

  83. @codeJENNerator
    Getting Started
    projects.spring.io/spring-restdocs

    View Slide

  84. @codeJENNerator
    Getting Started
    •Start with reading the docs; The written docs are good!

    •Overview

    •Sponsored by Pivotal

    •Project Lead - Andy Wilkinson

    •Current Version - 1.2.1 released May 12

    •Can work with many JVM languages including Java, Groovy, and even
    Kotlin

    View Slide

  85. @codeJENNerator
    Other Interesting Talks
    • Java & Spring Boot
    • Test-Driven Documentation with Spring REST Docs - Spring I/O 2016 Andy
    Wilkinson
    • Writing comprehensive and guaranteed up-to-date REST API documentation -
    SpringOne Platform 2016 Anders Evers

    View Slide

  86. @codeJENNerator
    Groovier Spring REST docs
    • Spring Boot
    • Ratpack
    • Grails

    View Slide

  87. @codeJENNerator

    View Slide

  88. @codeJENNerator

    View Slide

  89. @codeJENNerator

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  93. @codeJENNerator
    Groovier Spring REST docs
    Example - Spring Boot
    • Groovy Spring Boot Project
    + Asciidoctor Gradle plugin
    + MockMVC and documentation to Spock tests

    View Slide

  94. @codeJENNerator
    Groovier Spring REST docs
    Example - Spring Boot
    • Groovy Spring Boot Project
    + Asciidoctor Gradle plugin
    + MockMVC and documentation to Spock tests
    + Add to static assets during build and publish to GitHub
    pages

    View Slide

  95. @codeJENNerator
    Groovy Spring Boot App
    • Start with lazybones spring boot app
    • Add mock endpoints for example
    https://github.com/jlstrater/groovy-spring-boot-restdocs-example

    View Slide

  96. @codeJENNerator
    Endpoints
    @CompileStatic

    @RestController

    @RequestMapping('/greetings')

    @Slf4j

    class GreetingsController {

    @RequestMapping(method = RequestMethod.GET, produces = 'application/json')

    ResponseEntity> list(@RequestParam(required = false) String message) {


    }


    @RequestMapping(path= '/{id}', method = RequestMethod.GET,
    produces = 'application/json')

    ResponseEntity> getById(@PathVariable String id) {


    }


    @RequestMapping(method = RequestMethod.POST, produces = 'application/json',
    consumes = 'application/json')

    ResponseEntity> post(@RequestBody Greeting example) {


    }

    }

    View Slide

  97. @codeJENNerator
    Endpoints
    @CompileStatic

    @RestController

    @RequestMapping('/greetings')

    @Slf4j

    class GreetingsController {

    @RequestMapping(method = RequestMethod.GET, produces = 'application/json')

    ResponseEntity> list(@RequestParam(required = false) String message) {


    }


    @RequestMapping(path= '/{id}', method = RequestMethod.GET,
    produces = 'application/json')

    ResponseEntity> getById(@PathVariable String id) {


    }


    @RequestMapping(method = RequestMethod.POST, produces = 'application/json',
    consumes = 'application/json')

    ResponseEntity> post(@RequestBody Greeting example) {


    }

    }
    Get all greetings or search by name
    /greetings or
    /greetings?message=Hello

    View Slide

  98. @codeJENNerator
    Endpoints
    @CompileStatic

    @RestController

    @RequestMapping('/greetings')

    @Slf4j

    class GreetingsController {

    @RequestMapping(method = RequestMethod.GET, produces = 'application/json')

    ResponseEntity> list(@RequestParam(required = false) String message) {


    }


    @RequestMapping(path= '/{id}', method = RequestMethod.GET,
    produces = 'application/json')

    ResponseEntity> getById(@PathVariable String id) {


    }


    @RequestMapping(method = RequestMethod.POST, produces = 'application/json',
    consumes = 'application/json')

    ResponseEntity> post(@RequestBody Greeting example) {


    }

    }
    Get all greetings or search by name
    /greetings or
    /greetings?message=Hello
    Get a greeting by id
    /greetings/1

    View Slide

  99. @codeJENNerator
    Endpoints
    @CompileStatic

    @RestController

    @RequestMapping('/greetings')

    @Slf4j

    class GreetingsController {

    @RequestMapping(method = RequestMethod.GET, produces = 'application/json')

    ResponseEntity> list(@RequestParam(required = false) String message) {


    }


    @RequestMapping(path= '/{id}', method = RequestMethod.GET,
    produces = 'application/json')

    ResponseEntity> getById(@PathVariable String id) {


    }


    @RequestMapping(method = RequestMethod.POST, produces = 'application/json',
    consumes = 'application/json')

    ResponseEntity> post(@RequestBody Greeting example) {


    }

    }
    Get all greetings or search by name
    /greetings or
    /greetings?message=Hello
    Get a greeting by id
    /greetings/1
    Create a new greeting
    /greetings

    View Slide

  100. @codeJENNerator

    View Slide

  101. @codeJENNerator

    View Slide

  102. @codeJENNerator
    Asciidoctor Gradle
    Plugin

    View Slide

  103. @codeJENNerator
    Example AsciiDoc
    = Gr8Data API Guide

    Jenn Strater;

    :doctype: book

    :icons: font

    :source-highlighter: highlightjs

    :toc: left

    :toclevels: 4

    :sectlinks:


    [introduction]

    = Introduction


    The Gr8Data API is a RESTful web service for aggregating and displaying gender ratios from various
    companies across the world. This document outlines how to submit data from your company or team and

    how to access the aggregate data.


    [[overview-http-verbs]]

    == HTTP verbs


    The Gr8Data API tries to adhere as closely as possible to standard HTTP and REST conventions in its
    use of HTTP verbs.

    View Slide

  104. @codeJENNerator
    Install
    classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3'

    apply plugin: 'org.asciidoctor.convert'


    asciidoctor {

    sourceDir = file('src/docs')

    outputDir "$projectDir/src/main/resources/public"

    backends 'html5'

    attributes 'source-highlighter' : 'prettify',

    'imagesdir':'images',

    'toc':'left',

    'icons': 'font',

    'setanchors':'true',

    'idprefix':'',

    'idseparator':'-',

    'docinfo1':'true'

    }


    View Slide

  105. @codeJENNerator

    View Slide

  106. @codeJENNerator
    Converted to HTML

    View Slide

  107. @codeJENNerator

    View Slide

  108. @codeJENNerator

    View Slide

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

    View Slide

  110. @codeJENNerator
    Stand Alone Setup
    class ExampleControllerSpec extends Specification {

    protected MockMvc mockMvc


    void setup() {

    this.mockMvc = MockMvcBuilders.standaloneSetup(
    new ExampleController()).build()

    }


    void 'test and document get by message’() {

    when:

    ResultActions result = this.mockMvc.perform(get('/greetings')

    .param('message', ‘Hallo API Conference DE!’)

    .contentType(MediaType.APPLICATION_JSON))
    then:

    result

    .andExpect(status().isOk())

    .andExpect(jsonPath(‘message’).value(‘Hallo API Conference DE!’))
    }
    }

    View Slide

  111. @codeJENNerator
    Web Context Setup
    void setup() {

    this.mockMvc = MockMvcBuilders
    .webAppContextSetup(this.context)
    .build()

    }

    View Slide

  112. @codeJENNerator
    Web Context Setup
    void setup() {

    this.mockMvc = MockMvcBuilders
    .webAppContextSetup(this.context)
    .build()

    }
    If context is null,
    remember to add
    spock-spring!!

    View Slide

  113. @codeJENNerator

    View Slide

  114. @codeJENNerator

    View Slide

  115. @codeJENNerator
    Gradle
    ext {

    snippetsDir = file('build/generated-snippets')

    }

    testCompile “org.springframework.restdocs:spring-restdocs-mockmvc:$
    {springRestDocsVersion}"
    ext['spring-restdocs.version'] = '1.2.0.RELEASE'

    View Slide

  116. @codeJENNerator
    Gradle
    ext {

    snippetsDir = file('build/generated-snippets')

    }

    testCompile “org.springframework.restdocs:spring-restdocs-mockmvc:$
    {springRestDocsVersion}"
    ext['spring-restdocs.version'] = '1.2.0.RELEASE'
    Spring Boot
    Specific!

    View Slide

  117. @codeJENNerator
    asciidoctor {

    dependsOn test

    sourceDir = file('src/docs')

    outputDir "$projectDir/src/main/resources/public"

    + inputs.dir snippetsDir

    backends 'html5'

    attributes 'source-highlighter' : 'prettify',

    'imagesdir':'images',

    'toc':'left',

    'icons': 'font',

    'setanchors':'true',

    'idprefix':'',

    'idseparator':'-',

    'docinfo1':'true',

    + 'snippets': snippetsDir

    }
    Gradle

    View Slide

  118. @codeJENNerator
    With AsciiDoctor
    Module

    View Slide

  119. @codeJENNerator
    Gradle
    ext {

    snippetsDir = file('build/generated-snippets')

    }

    testCompile “org.springframework.restdocs:spring-restdocs-mockmvc:${springRestDocsVersion}"
    asciidoctor “org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}"

    test {

    outputs.dir snippetsDir

    }


    asciidoctor {

    dependsOn test

    inputs.dir snippetsDir

    }

    View Slide

  120. @codeJENNerator
    Gradle
    ext {

    snippetsDir = file('build/generated-snippets')

    }

    testCompile “org.springframework.restdocs:spring-restdocs-mockmvc:${springRestDocsVersion}"
    asciidoctor “org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}"

    test {

    outputs.dir snippetsDir

    }


    asciidoctor {

    dependsOn test

    inputs.dir snippetsDir

    }

    View Slide

  121. @codeJENNerator

    View Slide

  122. @codeJENNerator
    class GreetingsControllerSpec extends Specification {


    @Rule

    JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()


    protected MockMvc mockMvc


    void setup() {

    this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())

    .apply(documentationConfiguration(this.restDocumentation))

    .build()

    }

    View Slide

  123. @codeJENNerator
    class GreetingsControllerSpec extends Specification {


    @Rule

    JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()


    protected MockMvc mockMvc


    void setup() {

    this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())

    .apply(documentationConfiguration(this.restDocumentation))

    .build()

    }

    View Slide

  124. @codeJENNerator
    class GreetingsControllerSpec extends Specification {


    @Rule

    JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()


    protected MockMvc mockMvc


    void setup() {

    this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())

    .apply(documentationConfiguration(this.restDocumentation))

    .build()

    }

    View Slide

  125. @codeJENNerator
    class GreetingsControllerSpec extends Specification {


    @Rule

    JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()


    protected MockMvc mockMvc


    void setup() {

    this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())

    .apply(documentationConfiguration(this.restDocumentation))

    .build()

    }
    void 'test and document get by message'() {


    when:

    ResultActions result = this.mockMvc.perform(get('/greetings')

    .param('message', ‘Hello')

    .contentType(MediaType.APPLICATION_JSON))

    View Slide

  126. @codeJENNerator
    class GreetingsControllerSpec extends Specification {


    @Rule

    JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()


    protected MockMvc mockMvc


    void setup() {

    this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())

    .apply(documentationConfiguration(this.restDocumentation))

    .build()

    }
    then:

    result

    .andExpect(status().isOk())

    .andExpect(jsonPath(‘message').value('Hello'))
    .andDo(document('greetings-get-example', preprocessResponse(prettyPrint()),
    responseFields(greeting)))
    }
    FieldDescriptor[] greeting = new FieldDescriptor().with {

    [fieldWithPath('id').type(JsonFieldType.NUMBER)

    .description("The greeting's id"),

    fieldWithPath('message').type(JsonFieldType.STRING)

    .description("The greeting's message")]

    }

    }
    void 'test and document get by message'() {


    when:

    ResultActions result = this.mockMvc.perform(get('/greetings')

    .param('message', ‘Hello')

    .contentType(MediaType.APPLICATION_JSON))

    View Slide

  127. @codeJENNerator
    class GreetingsControllerSpec extends Specification {


    @Rule

    JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()


    protected MockMvc mockMvc


    void setup() {

    this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())

    .apply(documentationConfiguration(this.restDocumentation))

    .build()

    }
    then:

    result

    .andExpect(status().isOk())

    .andExpect(jsonPath(‘message').value('Hello'))
    .andDo(document('greetings-get-example', preprocessResponse(prettyPrint()),
    responseFields(greeting)))
    }
    FieldDescriptor[] greeting = new FieldDescriptor().with {

    [fieldWithPath('id').type(JsonFieldType.NUMBER)

    .description("The greeting's id"),

    fieldWithPath('message').type(JsonFieldType.STRING)

    .description("The greeting's message")]

    }

    }
    void 'test and document get by message'() {


    when:

    ResultActions result = this.mockMvc.perform(get('/greetings')

    .param('message', ‘Hello')

    .contentType(MediaType.APPLICATION_JSON))

    View Slide

  128. @codeJENNerator
    class GreetingsControllerSpec extends Specification {


    @Rule

    JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()


    protected MockMvc mockMvc


    void setup() {

    this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())

    .apply(documentationConfiguration(this.restDocumentation))

    .build()

    }
    then:

    result

    .andExpect(status().isOk())

    .andExpect(jsonPath(‘message').value('Hello'))
    .andDo(document('greetings-get-example', preprocessResponse(prettyPrint()),
    responseFields(greeting)))
    }
    FieldDescriptor[] greeting = new FieldDescriptor().with {

    [fieldWithPath('id').type(JsonFieldType.NUMBER)

    .description("The greeting's id"),

    fieldWithPath('message').type(JsonFieldType.STRING)

    .description("The greeting's message")]

    }

    }
    void 'test and document get by message'() {


    when:

    ResultActions result = this.mockMvc.perform(get('/greetings')

    .param('message', ‘Hello')

    .contentType(MediaType.APPLICATION_JSON))

    View Slide

  129. @codeJENNerator
    Special Use Case

    View Slide

  130. @codeJENNerator
    Setup Via Annotation

    +@WebMvcTest(controllers = GreetingsController)

    +@AutoConfigureRestDocs(

    + outputDir = "build/generated-snippets",

    + uriHost = “greetingsfromcodeeurope.pl",

    + uriPort = 8080

    )
    class BaseControllerSpec extends Specification {

    // @Rule

    // JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/
    docs/generated-snippets')


    + @Autowired

    protected MockMvc mockMvc

    //

    // @Autowired

    // private WebApplicationContext context

    //

    // void setup() {

    // this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)

    // .apply(documentationConfiguration(this.restDocumentation))

    // .build()

    // }
    }

    View Slide

  131. @codeJENNerator

    View Slide

  132. @codeJENNerator

    View Slide

  133. @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
    request body
    response body

    View Slide

  134. @codeJENNerator
    Example Http Response
    [source,http,options="nowrap"]

    ----

    HTTP/1.1 200 OK

    Content-Type: application/json;charset=UTF-8

    Content-Length: 37


    {

    "id" : 1,

    "message" : "Hello"

    }

    ----

    View Slide

  135. @codeJENNerator
    Example Response Fields
    |===

    |Path|Type|Description


    |`id`

    |`Number`

    |The greeting's id


    |`message`

    |`String`

    |The greeting's message


    |===

    View Slide

  136. @codeJENNerator

    View Slide

  137. @codeJENNerator
    Create a new greeting
    void 'test and document post with example endpoint and custom name'() {

    when:

    ResultActions result = this.mockMvc.perform(post('/greetings')

    .content(new ObjectMapper().writeValueAsString(
    new Greeting(message: ‘Hallo API Conference DE!')))

    .contentType(MediaType.APPLICATION_JSON))

    then:

    result

    .andExpect(status().isCreated())

    .andDo(document('greetings-post-example',
    requestFields([fieldWithPath(‘id').type(JsonFieldType.NULL)
    .optional().description("The greeting's id"),

    fieldWithPath('message').type(JsonFieldType.STRING)

    .description("The greeting's message")])

    ))

    }

    View Slide

  138. @codeJENNerator
    Create a new greeting
    void 'test and document post with example endpoint and custom name'() {

    when:

    ResultActions result = this.mockMvc.perform(post('/greetings')

    .content(new ObjectMapper().writeValueAsString(
    new Greeting(message: ‘Hallo API Conference DE!')))

    .contentType(MediaType.APPLICATION_JSON))

    then:

    result

    .andExpect(status().isCreated())

    .andDo(document('greetings-post-example',
    requestFields([fieldWithPath(‘id').type(JsonFieldType.NULL)
    .optional().description("The greeting's id"),

    fieldWithPath('message').type(JsonFieldType.STRING)

    .description("The greeting's message")])

    ))

    }

    View Slide

  139. @codeJENNerator
    List Greetings
    void 'test and document get of a list of greetings'() {

    when:

    ResultActions result = this.mockMvc.perform(get('/greetings')

    .contentType(MediaType.TEXT_PLAIN))


    then:

    result

    .andExpect(status().isOk())

    .andDo(document(‘greetings-list-example',
    preprocessResponse(prettyPrint()),

    responseFields(greetingList)

    ))

    }
    FieldDescriptor[] greetingList = new FieldDescriptor().with {

    [fieldWithPath('[].id').type(JsonFieldType.NUMBER).optional()

    .description("The greeting's id"),

    fieldWithPath('[].message').type(JsonFieldType.STRING)

    .description("The greeting's message")]

    }

    View Slide

  140. @codeJENNerator
    List Greetings
    void 'test and document get of a list of greetings'() {

    when:

    ResultActions result = this.mockMvc.perform(get('/greetings')

    .contentType(MediaType.TEXT_PLAIN))


    then:

    result

    .andExpect(status().isOk())

    .andDo(document(‘greetings-list-example',
    preprocessResponse(prettyPrint()),

    responseFields(greetingList)

    ))

    }
    FieldDescriptor[] greetingList = new FieldDescriptor().with {

    [fieldWithPath('[].id').type(JsonFieldType.NUMBER).optional()

    .description("The greeting's id"),

    fieldWithPath('[].message').type(JsonFieldType.STRING)

    .description("The greeting's message")]

    }

    View Slide

  141. @codeJENNerator
    Path Parameters
    void 'test and document getting a greeting by id'() {

    when:

    ResultActions result = this.mockMvc.perform(
    RestDocumentationRequestBuilders.get('/greetings/{id}', 1))


    then:

    result

    .andExpect(status().isOk())

    .andExpect(jsonPath('id').value(1))

    .andDo(document('greetings-get-by-id-example',

    preprocessResponse(prettyPrint()),

    pathParameters(parameterWithName(‘id')
    .description("The greeting's id")),

    responseFields(greeting)

    ))

    }

    View Slide

  142. @codeJENNerator
    Path Parameters
    void 'test and document getting a greeting by id'() {

    when:

    ResultActions result = this.mockMvc.perform(
    RestDocumentationRequestBuilders.get('/greetings/{id}', 1))


    then:

    result

    .andExpect(status().isOk())

    .andExpect(jsonPath('id').value(1))

    .andDo(document('greetings-get-by-id-example',

    preprocessResponse(prettyPrint()),

    pathParameters(parameterWithName(‘id')
    .description("The greeting's id")),

    responseFields(greeting)

    ))

    }

    View Slide

  143. @codeJENNerator

    View Slide

  144. @codeJENNerator
    Failing Tests

    View Slide

  145. @codeJENNerator
    Failing Tests

    View Slide

  146. @codeJENNerator
    Failing Tests

    View Slide

  147. @codeJENNerator
    +

    View Slide

  148. @codeJENNerator
    +

    View Slide

  149. @codeJENNerator
    Add Snippets to AsciiDoc
    == Errors


    Whenever an error response (status code >= 400) is returned, the body will contain a
    JSON object

    that describes the problem. The error object has the following structure:


    include::{snippets}/error-example/response-fields.adoc[]


    For example, a request that attempts to apply a non-existent tag to a note will
    produce a

    `400 Bad Request` response:


    include::{snippets}/error-example/http-response.adoc[]


    [[resources]]

    = Resources


    include::resources/example.adoc[]

    View Slide

  150. @codeJENNerator
    New in 1.2!

    View Slide

  151. @codeJENNerator

    View Slide

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

    View Slide

  153. @codeJENNerator
    Publishing Strategies

    View Slide

  154. @codeJENNerator
    Strategies
    • Hook in asciidoctor with the gradle build task
    • Run the asciidoctor test separately (but make sure to run AFTER the tests)

    View Slide

  155. @codeJENNerator

    View Slide

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

    View Slide

  157. @codeJENNerator
    Publish Docs to Github Pages
    publish.gradle
    buildscript {

    repositories {

    jcenter()

    }


    dependencies {

    classpath 'org.ajoberstar:gradle-git:1.1.0'

    }

    }


    apply plugin: 'org.ajoberstar.github-pages'


    githubPages {

    repoUri = 'gi[email protected]:jlstrater/groovy-spring-boot-restdocs-example.git'

    pages {

    from(file('build/resources/main/public/html5'))
    }

    }

    View Slide

  158. @codeJENNerator
    Publish Docs to Github Pages
    publish.gradle
    buildscript {

    repositories {

    jcenter()

    }


    dependencies {

    classpath 'org.ajoberstar:gradle-git:1.1.0'

    }

    }


    apply plugin: 'org.ajoberstar.github-pages'


    githubPages {

    repoUri = 'gi[email protected]:jlstrater/groovy-spring-boot-restdocs-example.git'

    pages {

    from(file('build/resources/main/public/html5'))
    }

    }
    If you use this method,
    remember to deploy docs at the
    same time as the project!

    View Slide

  159. @codeJENNerator

    View Slide

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

    View Slide

  161. @codeJENNerator
    Support for Rest-Assured

    View Slide

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

    View Slide

  163. @codeJENNerator

    View Slide

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

  165. @codeJENNerator

    View Slide

  166. @codeJENNerator

    View Slide

  167. @codeJENNerator
    restdocs.gradle
    dependencies {

    testCompile ‘org.springframework.restdocs:spring-restdocs-restassured:${springRestDocsVersion}'

    }


    ext {

    snippetsDir = file('build/generated-snippets')

    }
    test {

    outputs.dir snippetsDir

    }


    asciidoctor {

    mustRunAfter test

    inputs.dir snippetsDir

    sourceDir = file('src/docs')

    separateOutputDirs = false

    outputDir "$projectDir/src/ratpack/public/docs"

    attributes 'snippets': snippetsDir,

    'source-highlighter': 'prettify',

    'imagesdir': 'images',

    'toc': 'left',

    'icons': 'font',

    'setanchors': 'true',

    'idprefix': '',

    'idseparator': '-',

    'docinfo1': 'true'

    }


    build.dependsOn asciidoctor


    View Slide

  168. @codeJENNerator
    build.gradle
    plugins {

    id "io.ratpack.ratpack-groovy" version "1.2.0"

    .
    .
    .

    + id 'org.asciidoctor.convert' version '1.5.3'

    }


    repositories {

    jcenter()
    .
    .
    .
    }

    //some CI config

    apply from: "gradle/ci.gradle"

    + apply from: "gradle/restdocs.gradle"


    View Slide

  169. @codeJENNerator

    View Slide

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

    }

    }
    }

    View Slide

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

    }

    }
    }
    Get a book by ISBN
    /api/books/1932394842

    View Slide

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

    }

    }
    }
    Get a book by ISBN
    /api/books/1932394842
    Get all books
    /api/books

    View Slide

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

    }

    }
    }
    Get a book by ISBN
    /api/books/1932394842
    Get all books
    /api/books
    Post to create a new book
    /api/books

    View Slide

  174. @codeJENNerator

    View Slide

  175. @codeJENNerator
    Asciidoctor Gradle
    Plugin
    (same as before)

    View Slide

  176. @codeJENNerator

    View Slide

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

    View Slide

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

  179. @codeJENNerator
    class BookDocumentationSpec extends BaseDocumentationSpec {


    @Shared

    EmbeddedApp isbndb = GroovyEmbeddedApp.of {

    handlers {

    all {

    render '{"data" : [{"title" : "Learning Ratpack", "publisher_name" : "O\'Reilly Media", "author_data" : [{"id" :
    "dan_woods", "name" : "Dan Woods"}]}]}'

    }

    }

    }


    @Delegate

    TestHttpClient client = aut.httpClient

    RemoteControl remote = new RemoteControl(aut)



    def setupSpec() {

    System.setProperty('eb.isbndb.host', "http://${isbndb.address.host}:${isbndb.address.port}")

    System.setProperty('eb.isbndb.apikey', "fakeapikey")

    }


    def cleanupSpec() {

    System.clearProperty('eb.isbndb.host')

    }


    def setupTestBook() {

    requestSpec { RequestSpec requestSpec ->

    requestSpec.body.type("application/json")

    requestSpec.body.text(JsonOutput.toJson([isbn: "1932394842", quantity: 0, price: 22.34]))

    }

    post("api/book")

    }


    View Slide

  180. @codeJENNerator
    class BookDocumentationSpec extends BaseDocumentationSpec {


    @Shared

    EmbeddedApp isbndb = GroovyEmbeddedApp.of {

    handlers {

    all {

    render '{"data" : [{"title" : "Learning Ratpack", "publisher_name" : "O\'Reilly Media", "author_data" : [{"id" :
    "dan_woods", "name" : "Dan Woods"}]}]}'

    }

    }

    }


    @Delegate

    TestHttpClient client = aut.httpClient

    RemoteControl remote = new RemoteControl(aut)



    def setupSpec() {

    System.setProperty('eb.isbndb.host', "http://${isbndb.address.host}:${isbndb.address.port}")

    System.setProperty('eb.isbndb.apikey', "fakeapikey")

    }


    def cleanupSpec() {

    System.clearProperty('eb.isbndb.host')

    }


    def setupTestBook() {

    requestSpec { RequestSpec requestSpec ->

    requestSpec.body.type("application/json")

    requestSpec.body.text(JsonOutput.toJson([isbn: "1932394842", quantity: 0, price: 22.34]))

    }

    post("api/book")

    }


    Setup Test
    Data and
    Cleanup After
    Each Test

    View Slide

  181. @codeJENNerator
    void 'test and document list books'() {

    setup:

    setupTestBook()


    expect:

    given(this.documentationSpec)

    .contentType('application/json')

    .accept('application/json')

    .port(aut.address.port)

    .filter(document('books-list-example',

    preprocessRequest(modifyUris()

    .host('books.example.com')

    .removePort()),

    preprocessResponse(prettyPrint()),

    responseFields(

    fieldWithPath('[].isbn').description('The ISBN of the book'),

    fieldWithPath('[].quantity').description("The quantity of the book that is available"),

    fieldWithPath('[].price').description("The current price of the book"),

    fieldWithPath('[].title').description("The title of the book"),

    fieldWithPath('[].author').description('The author of the book'),

    fieldWithPath('[].publisher').description('The publisher of the book')

    )))

    .when()

    .get('/api/book')

    .then()

    .assertThat()

    .statusCode(is(200))

    }

    View Slide

  182. @codeJENNerator
    Response Body Snippet
    [source,options="nowrap"]

    ----

    [ {

    "isbn" : "1932394842",

    "quantity" : 0,

    "price" : 22.34,

    "title" : "Learning Ratpack",

    "author" : "Dan Woods",

    "publisher" : "O'Reilly Media"

    } ]

    ----

    View Slide

  183. @codeJENNerator

    View Slide

  184. @codeJENNerator

    View Slide

  185. @codeJENNerator
    Reusable Snippets
    protected final Snippet getBookFields() {

    responseFields(

    fieldWithPath('isbn').description('The ISBN of the book'),

    fieldWithPath('quantity').description("The quantity of the book that is available"),

    fieldWithPath('price').description("The current price of the book"),

    fieldWithPath('title').description("The title of the book"),

    fieldWithPath('author').description('The author of the book’),
    fieldWithPath('publisher').description('The publisher of the book’)
    )

    }

    View Slide

  186. @codeJENNerator
    def "test and document create book"() {

    given:

    def setup = given(this.documentationSpec)

    .body('{"isbn": "1234567890", "quantity": 10, "price": 22.34}')

    .contentType('application/json')

    .accept('application/json')

    .port(aut.address.port)

    .filter(document('books-create-example',

    preprocessRequest(prettyPrint(),

    modifyUris()

    .host('books.example.com')

    .removePort()),

    preprocessResponse(prettyPrint()),

    bookFields,

    requestFields(

    fieldWithPath('isbn').type(JsonFieldType.STRING).description('book ISBN id'),

    fieldWithPath('quantity').type(JsonFieldType.NUMBER).description('quanity available'),

    fieldWithPath('price').type(JsonFieldType.NUMBER)

    .description('price of the item as a number without currency')

    ),))

    when:

    def result = setup

    .when()

    .post("api/book")

    then:

    result

    .then()

    .assertThat()

    .statusCode(is(200))

    }

    View Slide

  187. @codeJENNerator
    def "test and document create book"() {

    given:

    def setup = given(this.documentationSpec)

    .body('{"isbn": "1234567890", "quantity": 10, "price": 22.34}')

    .contentType('application/json')

    .accept('application/json')

    .port(aut.address.port)

    .filter(document('books-create-example',

    preprocessRequest(prettyPrint(),

    modifyUris()

    .host('books.example.com')

    .removePort()),

    preprocessResponse(prettyPrint()),

    bookFields,

    requestFields(

    fieldWithPath('isbn').type(JsonFieldType.STRING).description('book ISBN id'),

    fieldWithPath('quantity').type(JsonFieldType.NUMBER).description('quanity available'),

    fieldWithPath('price').type(JsonFieldType.NUMBER)

    .description('price of the item as a number without currency')

    ),))

    when:

    def result = setup

    .when()

    .post("api/book")

    then:

    result

    .then()

    .assertThat()

    .statusCode(is(200))

    }

    View Slide

  188. @codeJENNerator
    Public APIs

    View Slide

  189. @codeJENNerator
    Blog Post

    View Slide

  190. @codeJENNerator
    Outcomes

    View Slide

  191. @codeJENNerator
    One Year Later
    • Made it to production! :)
    • Team still happy with Spring REST Docs
    • Other dev teams like to see the examples

    View Slide

  192. @codeJENNerator
    Read the docs for more on..
    • Adding Security and Headers
    • Documenting Constraints
    • Hypermedia Support
    • XML Support
    • Using Markdown instead of Asciidoc
    • Third Party Extensions for WireMock and Jersey

    View Slide

  193. @codeJENNerator
    Conclusion

    View Slide

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

    View Slide

  195. @codeJENNerator

    View Slide

  196. @codeJENNerator
    • API documentation is complex

    View Slide

  197. @codeJENNerator
    • API documentation is complex
    • Choosing the right tool for the job not just about the
    easiest one to setup

    View Slide

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

    View Slide

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

    View Slide

  200. @codeJENNerator
    Next Steps
    • Join the Groovy Community on Slack groovycommunity.com
    • Join #spring-restdocs on gitter https://gitter.im/spring-projects/spring-restdocs

    View Slide

  201. @codeJENNerator
    Questions?
    https://github.com/jlstrater/groovy-spring-boot-restdocs-example
    https://github.com/ratpack/example-books
    https://github.com/jlstrater/spring-restdocs-public-api-example
    https://flic.kr/p/5DeuzB

    View Slide