📜 ⬆️ ⬇️

We document and test the REST API using SpringRestDocs

Good afternoon, I want to touch on the topic of documenting the REST API. Immediately make a reservation, this material will be focused on engineers working in the Spring ecosystem.
On several recent projects, I used the SpringRestDocs framework, it was successfully entrenched in the portfolio, was shown to friends, who also began to successfully use it, and now I want to share with you in the article about its capabilities and benefits. The article will allow you to understand the use of SpringRestDocs and start using it.

From the moment of acquaintance with this tool, I realized that there was a solution, which I was waiting for and it was not enough in development. Judge for yourself - one could only dream about it:

  • Documentation is generated automatically when running tests.
  • You can control the original format of the documentation file — for example, compile html. Since we use Spring boot, we can modify the steps and tasks in the gradle, copy the documentation file and include it in the jar-ku, publish the documentation on the remote server, copy the documentation to the archive. Thus, you will always have a static endpoint with documentation wherever your service is located. For offline version you can connect pdf, epub, abook permissions.
  • The documentation of our REST service is consistent with the logic of work. The documentation is synchronized with the logic of the application. Made changes, forgot to reflect them in the documentation - instantly see the falling tests with a detailed description of the difference inconsistencies.
  • Documentation is generated from tests. Now, in order to add new sections to the documentation, or start to lead it - you need to start by writing a test, but a test. After all, very often developers, in conditions of lack of time, undelivered processes on a project, or other reasons, write tons of code, but do not pay attention to the importance of tests. Oddly enough, but the documentation framework encourages you to work on TDD
  • As a result, you maintain a high level of coverege. More precisely, the non-coverage percentages that will be drawn in code analysis systems or reports are important. It is important that you cover different scenarios with separate tests and include their execution results in the documentation. Green tests are always nice.
Let's deal with the work of SpringRestDocs, I will combine the material with theoretical inserts and lead a practical line of the tutorial, after reading which you can configure and use the framework.

SpringRestDocs Pipeline


In order to start working with SpringRestDocs, you need to understand the principle of its pipeline, it is quite simple and linear:
')
rest docs pipeline

All actions originate from tests, except for the resource verification logic, also snippets are generated. Snippets are a serialized value of a specific HTTP attribute with which our controller interacted. We prepare a special file template in which we indicate in which sections the generated snippets should be included. The output is a compiled documentation file, note that we can specify the format of the documentation - it can be in the format of html, pdf, epub, abook.

Further, in the text of the article we will assemble this pipeline, write tests, configure SpringRestDocs and compile the documentation.

Dependencies


Below are the dependencies from my project that works with spring rest docs, for example which we will analyze the work

dependencies { compile "org.springframework.boot:spring-boot-starter-data-jpa" compile "org.springframework.boot:spring-boot-starter-hateoas" compile "org.springframework.boot:spring-boot-starter-web" compile "org.springframework.restdocs:spring-restdocs-core:$restdocsVersion" compile "com.h2database:h2:$h2Version" compile "org.projectlombok:lombok" testCompile "org.springframework.boot:spring-boot-starter-test" asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" testCompile "com.jayway.jsonpath:json-path" } 

Tested Controller


I will show the part of the controller for which we will write a test, connect SpringRestDocs and generate the documentation.

 @RestController @RequestMapping("/speakers") public class SpeakerController { @Autowired private SpeakerRepository speakerRepository; @GetMapping(path = "/{id}") public ResponseEntity<SpeakerResource> getSpeaker(@PathVariable long id) { return speakerRepository.findOne(id) .map(speaker -> ResponseEntity.ok(new SpeakerResource(speaker))) .orElse(new ResponseEntity(HttpStatus.NOT_FOUND)); } 

Let's look at its logic. With the help of SpringDataRepository, I apply to the database for a record with the ID that was transferred to the controller. SpringDataRepository returns Optional - in case there is a value in it, we perform the transformation of the JPA Entity into a resource (we can encapsulate some of the fields that we don’t want to show in the response), but if Optional.isEmpty () then we return the 404 code NOT_FOUND .

ResourceResource resource code


 @NoArgsConstructor @AllArgsConstructor @Getter @Relation(value = "speaker", collectionRelation = "speakers") public class SpeakerResource extends ResourceSupport { private String name; private String company; public SpeakerResource(Speaker speaker) { this.name = speaker.getName(); this.company = speaker.getCompany(); add(linkTo(methodOn(SpeakerController.class).getSpeaker(speaker.getId())).withSelfRel()); add(linkTo(methodOn(SpeakerController.class).getSpeakerTopics(speaker.getId())).withRel("topics")); } } 

Write a basic test for this endpoint


 @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs(outputDir = "build/generated-snippets") public class SpControllerTest { @Autowired private MockMvc mockMvc; @Autowired private SpeakerRepository speakerRepository; @After public void tearDown() { speakerRepository.deleteAll(); } @Test public void testGetSpeaker() throws Exception { // Given Speaker speaker = Speaker.builder().name("Roman").company("Lohika").build(); speakerRepository.save(speaker); // When ResultActions resultActions = mockMvc.perform(get("/speakers/{id}", speaker.getId())) .andDo(print()); // Then resultActions.andExpect(status().isOk()) .andExpect(jsonPath("name", is("Roman"))) .andExpect(jsonPath("company", is("Lohika"))); } } 

In the test, I connect the mockMVC, RestDocs autoconfiguration. For restdocs, it is necessary to specify the directory where snippets will be generated (outputDir = "buid / generated-snippets") , this is a common test using mockMvc, which we write almost every day when we test rest services. I use the proprietary library from Dependancy spring.tests mockMvc, however, if you prefer to use RestAssured, then everything read will also be relevant - there are only minor modifications. My test performs a call to the HTTP method of the controller, verifies the status, fields, and prints the request / response flow to the console.

Resultshandler


After running the test in its output, we see the following:

 MockHttpServletRequest: HTTP Method = GET Request URI = /speakers/1 Parameters = {} Headers = {} Handler: Type = smartjava.domain.speaker.SpeakerController Method = public org.springframework.http.ResponseEntity<smartjava.domain.speaker.SpeakerResource> smartjava.domain.speaker.SpeakerController.getSpeaker(long) Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[application/hal+json;charset=UTF-8]} Content type = application/hal+json;charset=UTF-8 Body = { "name" : "Roman", "company" : "Lohika", "_links" : { "self" : { "href" : "http://localhost:8080/speakers/1" }, "topics" : { "href" : "http://localhost:8080/speakers/1/topics" } } } 

This is the output of the HTTP request and response contents to the console. Thus, we can trace which values ​​were transferred to our controller and what was the response from it. The output to the console is performed by the connected handler:

  resultActions.andDo(print()); 

ResultHandler is a functional interface. By creating our own implementation and connecting it in the test, we can access the HttpRequest / HttpResponse , which was performed in the test and interpret the execution results, if we wish to record these values ​​to the console, to the file system, to our own documentation file, and so on.

 public interface ResultHandler { /** * Perform an action on the given result. * * @param result the result of the executed request * @throws Exception if a failure occurs */ void handle(MvcResult result) throws Exception; } 

MvcResult


As we can see, ResultHandler has access and can interpret MvcResult values ​​- the object containing the mockMvc test execution results, and through it the attributes of two key players - MockHttpServletRequest, MockHttpServletResponse. Here is a partial list of these attributes:

mvcResults

Here is an example of MyResultHandler, which logs the type of the HTTP method called and the status code of the response:

 public class MyResultHandler implements ResultHandler { private Logger logger = LoggerFactory.getLogger(MyResultHandler.class); static public ResultHandler myHandler() { return new MyResultHandler(); } @Override public void handle(MvcResult result) throws Exception { MockHttpServletRequest request = result.getRequest(); MockHttpServletResponse response = result.getResponse(); logger.error("HTTP method: {}, status code: {}", request.getMethod(), response.getStatus()); } } 

  resultActions.andDo(new MyResultHandler()) 

It was this idea with processing and registration that Pivotal used to generate the documentation. We need to connect a handler from the MockMvcRestDocumentation class to our test:

 // Document resultActions.andDo(MockMvcRestDocumentation.document("{class-name}/{method-name}")); 

Let's generate snippets


Let's run the test again and note that after it has been executed, folders with files have been created in the build / generated-snippets directory:

 ./sp-controller-test/test-get-speaker: total 48 -rw-r--r-- 1 rtsypuk staff 68B Oct 31 14:17 curl-request.adoc -rw-r--r-- 1 rtsypuk staff 87B Oct 31 14:17 http-request.adoc -rw-r--r-- 1 rtsypuk staff 345B Oct 31 14:17 http-response.adoc -rw-r--r-- 1 rtsypuk staff 69B Oct 31 14:17 httpie-request.adoc -rw-r--r-- 1 rtsypuk staff 36B Oct 31 14:17 request-body.adoc -rw-r--r-- 1 rtsypuk staff 254B Oct 31 14:17 response-body.adoc 

These are the generated snippets. By default, rest docs generates 6 types of snippet, I will show some of them.

Snippet is a part of the HTTP request / response HTTP load that is serialized into a file in a textual representation. The most commonly used snippets are curl-request, http-request, http-response, request-body, response-body, links (for HATEOAS services), path-parameters, response-fields, headers.

curl-request.adoc

 [source,bash] ---- $ curl 'http://localhost:8080/speakers/1' -i ---- 


http-request.adoc
 [source,bash] [source,http,options="nowrap"] ---- GET /speakers/1 HTTP/1.1 Host: localhost:8080 ---- 

http-response.adoc

 [source,bash] [source,http,options="nowrap"] ---- HTTP/1.1 200 OK Content-Type: application/hal+json;charset=UTF-8 Content-Length: 218 { "name" : "Roman", "company" : "Lohika", "_links" : { "self" : { "href" : "http://localhost:8080/speakers/1" }, "topics" : { "href" : "http://localhost:8080/speakers/1/topics" } } } ---- 

Preparing a template file


Now you need to prepare a template file and mark up its sections, which will include generated snipeet blocks. The template is maintained in a flexible asciidoc format, by default the template should be located in the src / docs / asciidoc directory :

 == Rest convention include::etc/rest_conv.adoc[] == Endpoints === Speaker ==== Get speaker by ID ===== Curl example include::{snippets}/sp-controller-test/test-get-speaker/curl-request.adoc[] ===== HTTP Request include::{snippets}/sp-controller-test/test-get-speaker/http-request.adoc[] ===== HTTP Response ====== Success HTTP responses include::{snippets}/sp-controller-test/test-get-speaker/http-response.adoc[] ====== Response fields include::{snippets}/sp-controller-test/test-get-speaker/response-fields.adoc[] ====== HATEOAS links include::{snippets}/sp-controller-test/test-get-speaker/links.adoc[] 

Using the asciidoc syntax we can include static files (for example, in the rest_conv.adoc file I made a description of which methods supported the service, in what cases which status codes should be returned), as well as auto-generated snippets files.

Static rest_conv.adoc


 === HTTP verbs Speakers Service tries to adhere as closely as possible to standard HTTP and REST conventions in its use of HTTP verbs. |=== | Verb | Usage | `GET` | Used to retrieve a resource | `POST` | Used to create a new resource | `PATCH` | Used to update an existing resource, including partial updates | `PUT` | Used to update an existing resource, full updates only | `DELETE` | Used to delete an existing resource |=== === HTTP status codes Speakers Service tries to adhere as closely as possible to standard HTTP and REST conventions in its use of HTTP status codes. |=== | Status code | Usage | `200 OK` | Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request, the response will contain an entity describing or containing the result of the action. | `201 Created` | The request has been fulfilled and resulted in a new resource being created. | `204 No Content` | The server successfully processed the request, but is not returning any content. | `400 Bad Request` | The server cannot or will not process the request due to something that is perceived to be a client error (eg, malformed request syntax, invalid request message framing, or deceptive request routing). | `404 Not Found` | The requested resource could not be found but may be available again in the future. Subsequent requests by the client are permissible. | `409 Conflict` | The request could not be completed due to a conflict with the current state of the target resource. | `422 Unprocessable Entity` | Validation error has happened due to processing the posted entity. |=== 

Configuring build.gradle


In order for the documentation to be compiled it is necessary to make a basic configuration - to enable the necessary dependencies, in the gradle.build buildscript.dependencies you need to add asciidoctor-gradle-plugin

 buildscript { repositories { jcenter() mavenCentral() mavenLocal() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "org.asciidoctor:asciidoctor-gradle-plugin:$asciiDoctorPluginVersion" classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" } } 

Apply the plugin

 apply plugin: 'org.asciidoctor.convert' 

Now we need to make the basic configuration asciidoctor:

 asciidoctor { dependsOn test backends = ['html5'] options doctype: 'book' attributes = [ 'source-highlighter': 'highlightjs', 'imagesdir' : './images', 'toc' : 'left', 'toclevels' : 3, 'numbered' : '', 'icons' : 'font', 'setanchors' : '', 'idprefix' : '', 'idseparator' : '-', 'docinfo1' : '', 'safe-mode-unsafe' : '', 'allow-uri-read' : '', 'snippets' : snippetsDir, linkattrs : true, encoding : 'utf-8' ] inputs.dir snippetsDir outputDir "build/asciidoc" sourceDir 'src/docs/asciidoc' sources { include 'index.adoc' } } 

Check the build documentation, run in the console

 gradle asciidoctor 

since we have indicated that the asciidoctor task is dependent on running tests, tests will first be broken through, snippets will be generated and these snippets will be included in the generated documentation.

Documentation


All the described configuration steps must be performed once when the project is lifted. Now, every time we run tests, we will additionally generate snippets and generate documentation. Here are some screenshots:

Section of the agreement on HTTP methods and status codes

image

Example of Get All Speakers Method Documentation

image

Identical documentation is also available in pdf format. It is convenient as an offline version, can be sent along with the specifications of your service to customers.

image

Jar task modification


Well, since we are working with spring boot now we can use one of its interesting properties - all the resources that are in the src / static or src / public directory will be available as static content when accessed from the browser

 jar { dependsOn asciidoctor from ("${asciidoctor.outputDir}/html5") { include '**/index.html' include '**/images/*' into 'static/docs' } } 

This is what we will do - after the documentation is assembled, we will copy it to the / static / docs directory. Thus, each assembled jar artifact will contain a static endoint with documentation. Regardless of where it will be located, what environment it will be in - we will always have the latest version of the documentation available.

Conclusion


This is only a small part of the capabilities of this wonderful tool, it is impossible to cover everything in one article. For anyone interested in SpringRestDocs, I offer links to resources:

  • This is how the compiled documentation looks like; in this example, you can look at the asciidoc format, how powerful it is (by the way, the dock can be automatically uploaded to githubpages) tsypuk.imtqy.com/springrestdoc
  • my github with a customized demo project with SpringRestDocs github.com/tsypuk/springrestdoc (all configured, use the code in your projects for a quick start, asciidoctor demo syntax, examples of extensions, diagrams that can be easily generated and included in the documentation)
  • And of course the official documentation

Source: https://habr.com/ru/post/341636/


All Articles