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

test driven docs javaland 2019

test driven docs javaland 2019

Documenting RESTful APIs with Spring REST Docs
----
Documentation generated from source code is very popular. Solutions such as Swagger are available for many different languages and frameworks. However, limitations of annotation-based tools are becoming apparent. An overwhelming number of documentation annotations make for great docs but muddy the source code. Then, something changes and the docs are out of date again. That is where test-driven approaches come in.

Test-driven documentation solutions, such as Spring Rest Docs, generate example snippets for requests and responses from tests ensuring both code coverage and accurate documentation. It can even fail the build when documentation becomes out of date. This session will walk through how to implement test-driven documentation solutions. Examples will be in Spring Boot and Groovy, but the concepts are applicable to other ecosystems too. If time permits, the talk will also include how to document APIs that have been implemented using Spring Framework 5's WebFlux. Attendees should have a basic understanding of a markdown-like documentation tool such as AsciiDoc and how to construct RESTful APIs in a JVM ecosystem technology such as Spring Boot.
----
Jennifer 'Jenn' Strater is a Developer Advocate for Gradle. She is based out of Berlin, Germany.

jlstrater

March 18, 2019
Tweet

More Decks by jlstrater

Other Decks in Technology

Transcript

  1. @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.
  2. @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
  3. @codeJENNerator • Open Source • Build Automation • Commercial SaaS

    Product (on-premises) • Development Productivity • for Gradle AND Apache Maven Gradle Products
  4. @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
  5. @codeJENNerator • Creating RESTful APIs • Spring Boot • JavaEE/Jakarta/

    Microprofile • Ratpack • API Documentation • Wiki Pages, Word Documents, Confluence, etc • Asciidoc / Asciidoctor • Swagger / RAML Audience Background
  6. @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
  7. @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
  8. @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
  9. @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
  10. @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
  11. @codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation

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

    AsciiDoc Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First
  13. @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
  14. @codeJENNerator Custom Swagger Specification { "swagger": "2.0", "info": { "version":

    "1", "title": "My Service", "contact": { "name": "Company Name" }, "license": {} }, "host": "example.com", "basepath": "/docs" }
  15. @codeJENNerator • Generates a Swagger Specification from source • Is

    very easy to setup (in simple cases) • No OpenAPI Spec 3.0 support! SpringFox
  16. @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
  17. @codeJENNerator • In Swagger/OpenAPI Spec 2.0, there was no hypermedia

    support. • In OpenAPI Spec 3.0, there is some support for links Hypermedia
  18. @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 }
  19. @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
  20. @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
  21. @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
  22. @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 }
  23. @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"}]
  24. @codeJENNerator Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation

    AsciiDoc Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First
  25. @codeJENNerator • What problem are you trying to solve? •

    What kind of documentation do you need to solve that problem? Summary
  26. @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.
  27. @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', }
  28. @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
  29. @codeJENNerator • Ensures documentation matches implementation • Encourages writing more

    tests • Reduces duplication in docs and tests • Removes annotations from source Winning Solution
  30. @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
  31. @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
  32. @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
  33. @codeJENNerator Documentation Format • AsciiDoc • Markdown Sample Projects •

    Spring Boot • Grails • Slate • TestNG • And more! Out of the Box
  34. @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
  35. @codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping()

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

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

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

    Mono<Greeting> createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux<Greeting> listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono<Greeting> getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting List all greetings Get a greeting by id
  39. @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
  40. @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!!
  41. @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 }
  42. @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 }
  43. @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
  44. @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)
  45. @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)
  46. @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)
  47. @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 }
  48. @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 }
  49. @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
  50. @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
  51. @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
  52. @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
  53. @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
  54. @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
  55. @codeJENNerator Publish to GitHub Pages In build.gradle: plugins { id

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

    'org.ajoberstar.git-publish' version '2.0.0' } In publish.gradle: gitPublish { repoUri = '[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!
  57. @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()
 // } }
  58. @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
  59. @codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {


    get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List<Book> books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Groovier Spring REST Docs Example - Ratpack
  60. @codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {


    get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List<Book> books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842
  61. @codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {


    get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List<Book> books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842 Get all books /api/books
  62. @codeJENNerator path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {


    get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List<Book> books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Groovier Spring REST Docs Example - Ratpack Get a book by ISBN /api/books/1932394842 Get all books /api/books Post to create a new book /api/books
  63. @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()
 }
 }
  64. @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…
  65. @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
  66. @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
  67. @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