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

Test Driven Approaches to Documenting RESTful APIs

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.

jlstrater

February 09, 2016
Tweet

More Decks by jlstrater

Other Decks in Technology

Transcript

  1. Test-Driven Approaches
    to Documenting RESTful
    APIs
    Jenn Strater
    @jennstrater
    Groovy Users of Minnesota
    February 9, 2016

    View Slide

  2. About Me

    View Slide

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

    View Slide

  4. About Me
    • Senior Consultant at Object
    Partners, Inc.
    • Co-Founder of Gr8Ladies

    View Slide

  5. Background

    View Slide

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

    View Slide

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

    View Slide

  8. Current
    Documentation
    Models

    View Slide

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

    View Slide

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

    View Slide

  11. Endpoint Vs Resource
    Design

    View Slide

  12. Endpoint Vs Resource
    Design
    VS

    View Slide

  13. Swagger

    View Slide

  14. Swagger Approaches

    View Slide

  15. SpringFox
    src: https://www.flickr.com/photos/[email protected]/17125924230

    View Slide

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

    View Slide

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

    .

    .

    .

    }

    View Slide

  18. Swagger UI
    • Springfox generated UI
    • Copy static files and
    customize

    View Slide

  19. CONSIDERATIONS

    View Slide

  20. Central Information
    Security
    Http
    Verbs
    Error
    Handling
    Http
    Status

    View Slide

  21. Multiple Services

    View Slide

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

    View Slide

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

    View Slide

  24. Versioning

    View Slide

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

    View Slide

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

    View Slide

  27. Advantages
    “Try-It” Button

    View Slide

  28. Swagger2Markup

    View Slide

  29. public class Swagger2MarkupTest {

    @Autowired

    private WebApplicationContext context;


    private MockMvc mockMvc;


    @Before

    public void setUp() {

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

    }


    View Slide

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

    }


    View Slide

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

    }

    }

    View Slide

  32. View Slide

  33. View Slide

  34. Test-Driven Documentation
    Document Green
    Red Refactor

    View Slide

  35. Spring REST Docs

    View Slide

  36. Game Changers
    • Generated code snippets
    • Tests fail when documentation is missing or out-of-
    date
    • Supports Level III Rest APIs (Hypermedia)

    View Slide

  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

    View Slide

  38. Groovier Spring REST docs

    View Slide

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

    View Slide

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

    View Slide

  41. Groovier Spring REST docs
    • Groovy Spring Boot Project
    • Level II Rest API
    + Asciidoctor Gradle plugin
    + MockMVC and documentation to Spock tests

    View Slide

  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

    View Slide

  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

    }

    }

    View Slide

  44. Asciidoctor Gradle
    Plugin

    View Slide

  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'

    }


    View Slide

  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.

    View Slide

  47. Converted to HTML

    View Slide

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

    View Slide

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

    View Slide

  50. Web Context Setup
    void setup() {

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

    }

    View Slide

  51. Web Context Setup
    void setup() {

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

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

    View Slide

  52. Spring REST docs

    View Slide

  53. Gradle
    ext {

    snippetsDir = file(
    'src/docs/generated-snippets')

    }
    testCompile “org.springframework.restdocs:spring-
    restdocs-mockmvc:1.0.1.RELEASE”


    View Slide

  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,

    }

    View Slide

  55. Setup & GET

    View Slide

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

    }

    View Slide

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

    }

    View Slide

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

    View Slide

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

    ))

    }

    }

    View Slide

  60. View Slide

  61. POST

    View Slide

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

    View Slide

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

    ))

    }

    View Slide

  64. View Slide

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

    ))

    }

    View Slide

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

    ))

    }

    View Slide

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


    View Slide

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

    ))

    }

    View Slide

  69. Failing Tests

    View Slide

  70. Failing Tests

    View Slide

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

    View Slide

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

    View Slide

  73. Build Docs
    src/docs
    index.adoc
    src/main/
    resources/public/
    html5
    index.html

    View Slide

  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 = '[email protected]:jlstrater/groovy-spring-boot-restdocs-example.git'

    pages {

    from(file('build/docs'))

    }

    }

    View Slide

  75. http://jlstrater.github.io/groovy-spring-boot-restdocs-example

    View Slide

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

    View Slide

  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.

    View Slide

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

    View Slide

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

    View Slide