Getting Started With Spring REST Docs, Part Two
After we have finished this webinar, we:
- Know how we can document different HTTP status codes.
- Understand how we can document the JSON documents send to our REST API.
- Can document the HTTP responses returned by our REST API.
This webinar assumes that:
Introduction to System Under Test
During this webinar we will document a simple controller method that processes POST requests send to the path: ‘/api/message’. This method reads the information of the created message from the request body, validates the information of the created message, and returns the information of the “created” message as JSON.
The source code of the MessageController class looks as follows:
1import org.springframework.http.HttpStatus;
2import org.springframework.web.bind.annotation.*;
3
4import javax.validation.Valid;
5
6@RestController
7@RequestMapping("/api/message")
8class MessageController {
9
10 @RequestMapping(method = RequestMethod.POST)
11 @ResponseStatus(HttpStatus.CREATED)
12 MessageDTO create(@Valid @RequestBody MessageDTO message) {
13 return message;
14 }
15}
The MessageDTO class is a simple data transfer object that contains the information of a single message. It also ensures that we cannot create a message that has no message text. The source code of the MessageDTO class looks as follows:
1import javax.validation.constraints.NotBlank;
2
3public class MessageDTO {
4
5 @NotBlank
6 private String text;
7
8 public MessageDTO() {}
9
10 public String getText() {
11 return text;
12 }
13
14 public void setText(String text) {
15 this.text = text;
16 }
17}
Next, we will find out how how can create the required Asciidoctor snippets with Spring REST Docs.
Creating the Required Asciidoctor Snippets with Spring REST Docs
We can generate the required Asciidoctor snippets with Spring REST Docs by following these steps:
First, we have to create a new unit test class, configure Spring MVC Test framework, and integrate Spring REST Docs with Spring MVC Test framework. After we have done this, the source code of our test class looks as follows:
1import org.junit.jupiter.api.*;
2import org.junit.jupiter.api.extension.ExtendWith;
3import org.springframework.restdocs.RestDocumentationExtension;
4import org.springframework.test.web.servlet.MockMvc;
5import org.springframework.test.web.servlet.setup.MockMvcBuilders;
6
7import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
8
9@ExtendWith(RestDocumentationExtension.class)
10@Tag(TestTags.UNIT_TEST)
11class MessageControllerTest {
12
13 private MockMvc mockMvc;
14
15 @BeforeEach
16 void configureSystemUnderTest(RestDocumentationExtension restDocumentation) {
17 this.mockMvc = MockMvcBuilders.standaloneSetup(new MessageController())
18 .setControllerAdvice(new ValidationErrorHandler())
19 .apply(documentationConfiguration(restDocumentation))
20 .build();
21 }
22}
Second, we have create a nested test class which contains our test methods. After we have created this class, we have to add a MessageDTO field to the created class. This field contains the information of the created message.
After we have created the nested test class, the source code of the MessageControllerTest class looks as follows:
1import org.junit.jupiter.api.*;
2import org.junit.jupiter.api.extension.ExtendWith;
3import org.springframework.restdocs.RestDocumentationExtension;
4import org.springframework.test.web.servlet.MockMvc;
5import org.springframework.test.web.servlet.setup.MockMvcBuilders;
6
7import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
8
9@ExtendWith(RestDocumentationExtension.class)
10@Tag(TestTags.UNIT_TEST)
11class MessageControllerTest {
12
13 private MockMvc mockMvc;
14
15 @BeforeEach
16 void configureSystemUnderTest(RestDocumentationExtension restDocumentation) {
17 this.mockMvc = MockMvcBuilders.standaloneSetup(new MessageController())
18 .setControllerAdvice(new ValidationErrorHandler())
19 .apply(documentationConfiguration(restDocumentation))
20 .build();
21 }
22
23 @Nested
24 @DisplayName("Create a new message")
25 class CreateNewMessage {
26
27 private MessageDTO input;
28 }
29}
Third, we have to create the Asciidoctor snippets which help us to document what happens when we try to create a new message that has an empty message text. We can create these snippets by following these steps:
- Create a nested test class that contains our test methods.
- Write a setup method that creates a new
MessageDTOobject which has an empty message text. - Write a test method that creates the required Asciidoctor snippets.
After we have created the required Asciidoctor snippets, our test class looks as follows:
1import org.junit.jupiter.api.*;
2import org.junit.jupiter.api.extension.ExtendWith;
3import org.springframework.http.MediaType;
4import org.springframework.restdocs.RestDocumentationExtension;
5import org.springframework.test.web.servlet.MockMvc;
6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
7
8import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
9import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
10import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
11
12@ExtendWith(RestDocumentationExtension.class)
13@Tag(TestTags.UNIT_TEST)
14class MessageControllerTest {
15
16 private MockMvc mockMvc;
17
18 @BeforeEach
19 void configureSystemUnderTest(RestDocumentationExtension restDocumentation) {
20 this.mockMvc = MockMvcBuilders.standaloneSetup(new MessageController())
21 .setControllerAdvice(new ValidationErrorHandler())
22 .apply(documentationConfiguration(restDocumentation))
23 .build();
24 }
25
26 @Nested
27 @DisplayName("Create a new message")
28 class CreateNewMessage {
29
30 private MessageDTO input;
31
32 @Nested
33 @DisplayName("When empty message text is given")
34 class WhenEmptyMessageTextIsGiven {
35
36 @BeforeEach
37 void createInput() {
38 input = new MessageDTO();
39 input.setText("");
40 }
41
42 @Test
43 @DisplayName("Create the Spring REST Docs snippets")
44 void createSpringRESTDocsSnippets() throws Exception {
45 mockMvc.perform(post("/api/message")
46 .contentType(MediaType.APPLICATION_JSON_UTF8)
47 .content(WebTestUtil.convertObjectToJsonBytes(input))
48 )
49 .andDo(document("create-message-empty-text"));
50 }
51 }
52 }
53}
Fourth, we have to create the Asciidoctor snippets which help us to document what happens when we create a new message by using valid information. We can create these snippets by following these steps:
- Create a nested test class that contains our test methods.
- Write a setup method that creates a new
MessageDTOobject which has a valid message text. - Write a test method that creates the required Asciidoctor snippets.
After we have created the required Asciidoctor snippets, our test class looks as follows:
1import org.junit.jupiter.api.*;
2import org.junit.jupiter.api.extension.ExtendWith;
3import org.springframework.http.MediaType;
4import org.springframework.restdocs.RestDocumentationExtension;
5import org.springframework.test.web.servlet.MockMvc;
6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
7
8import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
9import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
10import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
11
12@ExtendWith(RestDocumentationExtension.class)
13@Tag(TestTags.UNIT_TEST)
14class MessageControllerTest {
15
16 private MockMvc mockMvc;
17
18 @BeforeEach
19 void configureSystemUnderTest(RestDocumentationExtension restDocumentation) {
20 this.mockMvc = MockMvcBuilders.standaloneSetup(new MessageController())
21 .setControllerAdvice(new ValidationErrorHandler())
22 .apply(documentationConfiguration(restDocumentation))
23 .build();
24 }
25
26 @Nested
27 @DisplayName("Create a new message")
28 class CreateNewMessage {
29
30 private MessageDTO input;
31
32 @Nested
33 @DisplayName("When empty message text is given")
34 class WhenEmptyMessageTextIsGiven {
35
36 @BeforeEach
37 void createInput() {
38 input = new MessageDTO();
39 input.setText("");
40 }
41
42 @Test
43 @DisplayName("Create the Spring REST Docs snippets")
44 void createSpringRESTDocsSnippets() throws Exception {
45 mockMvc.perform(post("/api/message")
46 .contentType(MediaType.APPLICATION_JSON_UTF8)
47 .content(WebTestUtil.convertObjectToJsonBytes(input))
48 )
49 .andDo(document("create-message-empty-text"));
50 }
51 }
52
53 @Nested
54 @DisplayName("When valid message text is given")
55 class WhenValidMessageTextIsGiven {
56
57 @BeforeEach
58 void createInput() {
59 input = new MessageDTO();
60 input.setText("Hello World!");
61 }
62
63 @Test
64 @DisplayName("Create the Spring REST Docs snippets")
65 void createSpringRESTDocsSnippets() throws Exception {
66 mockMvc.perform(post("/api/message")
67 .contentType(MediaType.APPLICATION_JSON_UTF8)
68 .content(WebTestUtil.convertObjectToJsonBytes(input))
69 )
70 .andDo(document("create-message-success"));
71 }
72 }
73 }
74}
Let’s move on and write the Asciidoctor document which documents our REST API.
Writing Our API Documentation
When we document our REST API, we will use the following Asciidoctor snippets:
- The request-body.adoc snippet describes the body of the HTTP request that is send to our REST API.
- The http-response.adoc snippet describes the HTTP response returned our REST API.
We can document our REST API by making some changes to the Asciidoctor document which created during the previous webinar. We can make these changes by following these steps:
First, we have to document the happy path of our API resource. We can do this by following these steps:
- Add a subsection called: ‘Create Message’ to the ‘Resources’ section.
- Describe how we can create a new message.
- Describe the legal HTTP status codes. It’s good idea to use a table for this purpose because it allows us to get a quick overview of the supported HTTP status codes.
- Describe the HTTP response that is returned by our API resource.
After we have documented the happy path of our API resource, the relevant part of our Asciidoctor document looks as follows:
1= Spring REST Docs Example
2Petri Kainulainen;
3:doctype: book
4:icons: font
5:source-highlighter: highlightjs
6:toc: left
7:toclevels: 4
8:sectlinks:
9
10[[resources]]
11= Resources
12
13[[resources-create-message]]
14== Create Message
15
16We can create a new message by sending a `POST` request to
17the path: `/api/message`. When we create a new message, we
18have to add the information of the created message to the
19request body.
20
21The JSON document that specifies the information of the created
22message looks as follows:
23
24include::{snippets}/create-message-success/request-body.adoc[]
25
26After our REST API has processed the incoming HTTP request, it
27identifies the outcome by returning an appropriate HTTP status code.
28The HTTP status codes used by this API method are described in the
29following table:
30
31[options="header"]
32|===============================================================
33| HTTP Status Code | Description
34| 201 | A new message was created successfully.
35| 400 | A new message was not created because validation failed.
36|===============================================================
37
38After our REST API has created a new message, it returns the
39information of the created message as JSON. The returned HTTP
40response looks as follows:
41
42include::{snippets}/create-message-success/http-response.adoc[]
Second, we have to describe what happens if we try to create a new message by using invalid information. We can do this by adding a subsection called ‘Validation Errors’ to the ‘Create Message’ section. After we have created this subsection, we have to document all validation errors by following these steps:
- Add a subsection called ‘Empty Message Text’ to the ‘Validation Errors’ section.
- Describe the JSON document that triggers this validation error.
- Describe the HTTP response that is returned by our REST API.
After we have described all validation errors, the relevant part of our Asciidoctor document looks as follows:
1= Spring REST Docs Example
2Petri Kainulainen;
3:doctype: book
4:icons: font
5:source-highlighter: highlightjs
6:toc: left
7:toclevels: 4
8:sectlinks:
9
10[[resources]]
11= Resources
12
13[[resources-create-message]]
14== Create Message
15
16We can create a new message by sending a `POST` request to
17the path: `/api/message`. When we create a new message, we
18have to add the information of the created message to the
19request body.
20
21The JSON document that specifies the information of the created
22message looks as follows:
23
24include::{snippets}/create-message-success/request-body.adoc[]
25
26After our REST API has processed the incoming HTTP request, it
27identifies the outcome by returning an appropriate HTTP status code.
28The HTTP status codes used by this API method are described in the
29following table:
30
31[options="header"]
32|===============================================================
33| HTTP Status Code | Description
34| 201 | A new message was created successfully.
35| 400 | A new message was not created because validation failed.
36|===============================================================
37
38After our REST API has created a new message, it returns the
39information of the created message as JSON. The returned HTTP
40response looks as follows:
41
42include::{snippets}/create-message-success/http-response.adoc[]
43
44=== Validation Errors
45
46When validation fails, our REST API returns the validation errors
47as JSON. This section describes all validation errors that can occur
48when we create a new message by using invalid information.
49
50==== Empty Message Text
51
52We cannot create a message that has an empty message text. In other
53words, when we send the following JSON document to our REST API:
54
55include::{snippets}/create-message-empty-text/request-body.adoc[]
56
57Our REST API returns the following HTTP response back to the client:
58
59include::{snippets}/create-message-empty-text/http-response.adoc[]
We have now documented our REST API. Let’s summarize what we learned from this webinar.
Summary
This webinar has taught us five things:
- If we have to document multiple different use cases of the same API resource, we have to ensure that each use case has a unique resource identifier.
- We can document the JSON document send to our REST API by using an Asciidoctor snippet called: request-body.adoc.
- It’s a good idea to document all supported HTTP status codes by using a table because it allows to get a quick overview of the supported HTTP status codes.
- We can document the HTTP response returned by our REST API by using an Asciidoctor snippet called: http-response.adoc.
- We should document all validation errors by using one subsection per validation error.