Save 37% off PRO during our Black Friday Sale! »

Test Driven Approaches to Documenting RESTful APIs

1f28a0c1988421be3268026bd6bb6f49?s=47 jlstrater
February 09, 2016

Test Driven Approaches to Documenting RESTful APIs

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 for groovy ecosystem technologies like Spring Boot. Attendees should have a basic understanding of AsciiDoc and how to construct RESTful APIs in Spring Boot or Grails.

1f28a0c1988421be3268026bd6bb6f49?s=128

jlstrater

February 09, 2016
Tweet

Transcript

  1. Test-Driven Approaches to Documenting RESTful APIs Jenn Strater @jennstrater Groovy

    Users of Minnesota February 9, 2016
  2. About Me

  3. About Me • Senior Consultant at Object Partners, Inc.

  4. About Me • Senior Consultant at Object Partners, Inc. •

    Co-Founder of Gr8Ladies
  5. Background

  6. Background • Creating RESTful APIs with Groovy • Spring Boot

    • Grails
  7. Background • Creating RESTful APIs with Groovy • Spring Boot

    • Grails • Documentation • Swagger • Asciidoctor
  8. Current Documentation Models

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

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

  11. Endpoint Vs Resource Design

  12. Endpoint Vs Resource Design VS

  13. Swagger

  14. Swagger Approaches

  15. SpringFox src: https://www.flickr.com/photos/24874528@N04/17125924230

  16. Super Easy Install dependencies { compile "io.springfox:springfox-swagger2:$springfoxVersion" compile “io.springfox:springfox-swagger-ui:$springfoxVersion” }

  17. Custom JSON {
 "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":
 .
 .
 .
 }
  18. Swagger UI • Springfox generated UI • Copy static files

    and customize
  19. CONSIDERATIONS

  20. Central Information Security Http Verbs Error Handling Http Status

  21. Multiple Services

  22. Headers src: https://cask.scotch.io/2015/06/angular-laravel-auth-10.png

  23. Object Mapping src: https://github.com/springfox/springfox/issues/281

  24. Versioning

  25. Annotation Hell src: http://www.nvisia.com/software-development/swagger-cataloging-with-the-worlds-most-popular-framework-for-apis

  26. Annotation Hell src: http://www.nvisia.com/software-development/swagger-cataloging-with-the-worlds-most-popular-framework-for-apis

  27. Advantages “Try-It” Button

  28. Swagger2Markup

  29. public class Swagger2MarkupTest {
 @Autowired
 private WebApplicationContext context;
 
 private

    MockMvc mockMvc;
 
 @Before
 public void setUp() {
 this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
 }
 

  30. public class Swagger2MarkupTest {
 @Autowired
 private WebApplicationContext context;
 
 private

    MockMvc mockMvc;
 
 @Before
 public void setUp() {
 this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
 }
 
 @Test
 public void convertSwaggerToAsciiDoc() throws Exception {
 this.mockMvc.perform(get("/v2/api-docs")
 .accept(MediaType.APPLICATION_JSON))
 .andDo(Swagger2MarkupResultHandler.outputDirectory(“src/docs/ asciidoc/generated").build())
 .andExpect(status().isOk());
 }
 

  31. public class Swagger2MarkupTest {
 @Autowired
 private WebApplicationContext context;
 
 private

    MockMvc mockMvc;
 
 @Before
 public void setUp() {
 this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
 }
 
 @Test
 public void convertSwaggerToAsciiDoc() throws Exception {
 this.mockMvc.perform(get("/v2/api-docs")
 .accept(MediaType.APPLICATION_JSON))
 .andDo(Swagger2MarkupResultHandler.outputDirectory(“src/docs/ asciidoc/generated").build())
 .andExpect(status().isOk());
 }
 
 @Test
 public void convertSwaggerToMarkdown() throws Exception {
 this.mockMvc.perform(get("/v2/api-docs")
 .accept(MediaType.APPLICATION_JSON))
 .andDo(Swagger2MarkupResultHandler.outputDirectory(“src/docs/ markdown/generated")
 .withMarkupLanguage(MarkupLanguage.MARKDOWN).build())
 .andExpect(status().isOk());
 }
 }
  32. None
  33. None
  34. Test-Driven Documentation Document Green Red Refactor

  35. Spring REST Docs

  36. Game Changers • Generated code snippets • Tests fail when

    documentation is missing or out-of- date • Supports Level III Rest APIs (Hypermedia)
  37. Getting Started • Documentation - Spring Projects • Documenting RESTful

    Apis - SpringOne2GX 2015 - Andy Wilkinson • Spring REST Docs - Documenting RESTful APIs using your tests - Devoxx Belgium 2015 - Anders Evers
  38. Groovier Spring REST docs

  39. Groovier Spring REST docs • Groovy Spring Boot Project •

    Level II Rest API
  40. Groovier Spring REST docs • Groovy Spring Boot Project •

    Level II Rest API + Asciidoctor Gradle plugin
  41. Groovier Spring REST docs • Groovy Spring Boot Project •

    Level II Rest API + Asciidoctor Gradle plugin + MockMVC and documentation to Spock tests
  42. Groovy Spring Boot App • Start with lazybones spring boot

    app • Add mock endpoints for example https://github.com/jlstrater/groovy-spring-boot- restdocs-example
  43. Endpoints @CompileStatic
 @RestController
 @RequestMapping('/hello')
 @Slf4j
 class ExampleController {
 
 @RequestMapping(method

    = RequestMethod.GET, produces = 'application/json', consumes = 'application/json')
 Example list() {
 new Example(name: 'World')
 }
 
 @RequestMapping(method = RequestMethod.POST, produces = 'application/json', consumes = 'application/json')
 Example list(@RequestBody Example example) {
 example
 }
 }
  44. Asciidoctor Gradle Plugin

  45. 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'
 }

  46. 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.
  47. Converted to HTML

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

  49. Stand Alone Setup class ExampleControllerSpec extends Specification {
 protected MockMvc

    mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup( new ExampleController()).build()
 }
 
 void 'test and document get with example endpoint’() {
 when:
 ResultActions result = this.mockMvc.perform( get(‘/hello’).contentType(MediaType.APPLICATION_JSON)) then:
 result .andExpect(status().isOk()) .andExpect(jsonPath("name").value("World"))
 .andExpect(jsonPath("message").value("Hello, World!”)) }
  50. Web Context Setup void setup() {
 this.mockMvc = MockMvcBuilders .webAppContextSetup(this.context)

    .build()
 }
  51. Web Context Setup void setup() {
 this.mockMvc = MockMvcBuilders .webAppContextSetup(this.context)

    .build()
 } If context is null, remember to add spock-spring!!
  52. Spring REST docs

  53. Gradle ext {
 snippetsDir = file( 'src/docs/generated-snippets')
 } testCompile “org.springframework.restdocs:spring-

    restdocs-mockmvc:1.0.1.RELEASE”

  54. Gradle 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,
 }
  55. Setup & GET

  56. Setup & GET class ExampleControllerSpec extends Specification {
 
 @Rule


    RestDocumentation restDocumentation = new RestDocumentation('src/docs/generated- snippets')
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 }
  57. Setup & GET class ExampleControllerSpec extends Specification {
 
 @Rule


    RestDocumentation restDocumentation = new RestDocumentation('src/docs/generated- snippets')
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 }
  58. Setup & GET class ExampleControllerSpec extends Specification {
 
 @Rule


    RestDocumentation restDocumentation = new RestDocumentation('src/docs/generated- snippets')
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 } void 'test and document get with example endpoint'() {
 when:
 ResultActions result = this.mockMvc.perform(get('/hello')
 .contentType(MediaType.APPLICATION_JSON))
  59. Setup & GET class ExampleControllerSpec extends Specification {
 
 @Rule


    RestDocumentation restDocumentation = new RestDocumentation('src/docs/generated- snippets')
 
 protected MockMvc mockMvc
 
 void setup() {
 this.mockMvc = MockMvcBuilders.standaloneSetup(new ExampleController())
 .apply(documentationConfiguration(this.restDocumentation))
 .build()
 } void 'test and document get with example endpoint'() {
 when:
 ResultActions result = this.mockMvc.perform(get('/hello')
 .contentType(MediaType.APPLICATION_JSON)) then:
 result
 .andExpect(status().isOk()) .andExpect(jsonPath('name').value('World'))
 .andExpect(jsonPath('message').value('Hello, World!'))
 .andDo(document('hello-get-example', preprocessResponse(prettyPrint()),
 responseFields(
 fieldWithPath('name').type(JsonFieldType.STRING).description('name'),
 fieldWithPath('message').type(JsonFieldType.STRING).description('hello world'))
 ))
 }
 }
  60. None
  61. POST

  62. POST void 'test and document post with example endpoint and

    custom name'() {
 when: ResultActions result = this.mockMvc.perform(post(‘/hello') .content(new ObjectMapper() .writeValueAsString(new Example(name: 'mockmvc test’)) .contentType(MediaType.APPLICATION_JSON))
  63. POST void 'test and document post with example endpoint and

    custom name'() {
 when: ResultActions result = this.mockMvc.perform(post(‘/hello') .content(new ObjectMapper() .writeValueAsString(new Example(name: 'mockmvc test’)) .contentType(MediaType.APPLICATION_JSON)) then:
 result
 .andExpect(status().isOk())
 .andExpect(jsonPath('name').value('mockmvc test'))
 .andExpect(jsonPath('message').value('Hello, mockmvc test!')) .andDo(document('hello-post-example', preprocessResponse(prettyPrint()),
 responseFields(
 fieldWithPath('name').type(JsonFieldType.STRING)
 .description('name'),
 fieldWithPath('message').type(JsonFieldType.STRING)
 .description('hello world'))
 ))
 }
  64. None
  65. List Example void 'test and document get of a list

    from example endpoint'() {
 when:
 ResultActions result = this.mockMvc.perform(get('/hello/list')
 .contentType(MediaType.APPLICATION_JSON))
 
 then:
 result
 .andExpect(status().isOk())
 .andDo(document('hello-list-example', preprocessResponse(prettyPrint()),
 responseFields(
 fieldWithPath('[].message').type(JsonFieldType.STRING)
 .description('message'))
 ))
 }
  66. List Example void 'test and document get of a list

    from example endpoint'() {
 when:
 ResultActions result = this.mockMvc.perform(get('/hello/list')
 .contentType(MediaType.APPLICATION_JSON))
 
 then:
 result
 .andExpect(status().isOk())
 .andDo(document('hello-list-example', preprocessResponse(prettyPrint()),
 responseFields(
 fieldWithPath('[].message').type(JsonFieldType.STRING)
 .description('message'))
 ))
 }
  67. Central Info - Errors void 'test and document error format’()

    { when:
 ResultActions result = this.mockMvc.perform(put('/error')
 .contentType(MediaType.APPLICATION_JSON)
 .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 405)
 .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, '/hello')
 .requestAttr(RequestDispatcher.ERROR_MESSAGE, "Request method 'PUT' not supported"))

  68. Central Info - Errors void 'test and document error format’()

    { when:
 ResultActions result = this.mockMvc.perform(put('/error')
 .contentType(MediaType.APPLICATION_JSON)
 .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 405)
 .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, '/hello')
 .requestAttr(RequestDispatcher.ERROR_MESSAGE, "Request method 'PUT' not supported"))
 then:
 result .andExpect(status().isMethodNotAllowed())
 .andDo(document('error-example', preprocessResponse(prettyPrint()),
 responseFields(
 fieldWithPath(‘error') .description('The HTTP error that occurred, e.g. `Bad Request`'),
 fieldWithPath(‘message') .description('A description of the cause of the error'),
 fieldWithPath('path').description('The path to which the request was made'),
 fieldWithPath('status').description('The HTTP status code, e.g. `400`'),
 fieldWithPath(‘timestamp') .description('The time, in milliseconds, at which the error occurred'))
 ))
 }
  69. Failing Tests

  70. Failing Tests

  71. Generated Snippets • curl-request.adoc • http-request.adoc • http-response.adoc • response-fields.adoc

    • request-parameters.adoc
  72. 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[]
  73. Build Docs src/docs index.adoc src/main/ resources/public/ html5 index.html

  74. Publish Docs publish.gradle buildscript {
 repositories {
 jcenter()
 }
 


    dependencies {
 classpath 'org.ajoberstar:gradle-git:1.1.0'
 }
 }
 
 apply plugin: 'org.ajoberstar.github-pages'
 
 githubPages {
 repoUri = 'git@github.com:jlstrater/groovy-spring-boot-restdocs-example.git'
 pages {
 from(file('build/docs'))
 }
 }
  75. http://jlstrater.github.io/groovy-spring-boot-restdocs-example

  76. Grails Rest Assured - 1.1.0 Release img src: https://www.flickr.com/photos/dskley/15558668690

  77. Conclusion • API documentation is complex • Choosing the right

    tool for the job not just about the easiest one to setup • Spring REST Docs is a promising tool to enforce good testing and documentation practices without muddying source code.
  78. Questions? https://github.com/jlstrater/groovy-spring-boot-restdocs-example @jennstrater

  79. Questions? https://github.com/jlstrater/groovy-spring-boot-restdocs-example @jennstrater