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: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.
- 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.
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" }
@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)); }
@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")); } }
@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"))); } }
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" } } }
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; }
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())
// Document resultActions.andDo(MockMvcRestDocumentation.document("{class-name}/{method-name}"));
./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
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.
[source,bash] ---- $ curl 'http://localhost:8080/speakers/1' -i ----
[source,bash] [source,http,options="nowrap"] ---- GET /speakers/1 HTTP/1.1 Host: localhost:8080 ----
[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" } } } ----
== 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[]
=== 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. |===
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 plugin: 'org.asciidoctor.convert'
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' } }
gradle asciidoctor
jar { dependsOn asciidoctor from ("${asciidoctor.outputDir}/html5") { include '**/index.html' include '**/images/*' into 'static/docs' } }
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