Last year I found out about spring restdocs, I really liked the idea so I created a small example and I wrote a post about it. That example is a simple REST service that exposes one resource and allows only GET operation on it. Last week I started a personal project that is also a REST service but exposes more resources and allows more HTTP operations on them so it is much better suited for a post about spring restdocs. In previous post I described all steps needed to setup a project for spring restdocs so in this one I will not repeat them and I will focus only in differences between them.
The first difference is related with how to run junit tests in a spring-boot project. In version 1.3.5.RELEASE it was like below:
1 2 3 4 5 6 |
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class GreetingControllerTest { } |
and in version 1.4.3.RELEASE it changed to:
1 2 3 4 5 |
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) public class BooksControllerTest { } |
The second difference is how RestDocumentation
junit rule is configured with the output directory into which generated snippets should be written. It changed from:
1 2 3 |
@Rule public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); |
into:
1 2 3 |
@Rule public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); |
In previous post I documented request parameters and response fields, in this post I will document path parameters, request fields, response fields and response headers.
One of the resources of this project is /users/{user}/books
which:
- returns all books for provided user on GET operation
- adds a new book for provided user on POST operation
Return all books for provided user
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@GetMapping(value = "/users/{user}/books") public ResponseEntity<List<Book>> getUserBooks(@PathVariable("user") String user) { try { logger.debug("Look for books for user {}", user); List<Book> userBooks = booksDao.getUserBooks(user); return new ResponseEntity<>(userBooks, HttpStatus.OK); } catch (Exception ex) { logger.error("Error on looking for books", ex); return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); } } |
This method needs to document path parameters and response fields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@Test public void getUserBooks() throws Exception { ArrayList<Book> books = new ArrayList<>(); books.add(getBook("1e4014b1-a551-4310-9f30-590c3140b695.json")); when(booksDao.getUserBooks(JOHN_DOE_USER)).thenReturn(books); this.mockMvc.perform(get("/users/{user}/books", JOHN_DOE_USER)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("$[0].uuid", is("1e4014b1-a551-4310-9f30-590c3140b695"))) .andExpect(jsonPath("$[0].isbn10", is("1-61729-310-5"))) .andExpect(jsonPath("$[0].isbn13", is("978-1-61729-310-8"))) .andExpect(jsonPath("$[0].title", is("Get Programming with JavaScript"))) .andExpect(jsonPath("$[0].authors[0].firstName", is("John R."))) .andExpect(jsonPath("$[0].authors[0].lastName", is("Larsen"))) .andExpect(jsonPath("$[0].pages", is(406))) .andDo(document("{class-name}/{method-name}", pathParameters(parameterWithName("user").description("User id")), responseFields( fieldWithPath("[].uuid").description("UUID used to identify a book"), fieldWithPath("[].isbn10").description("10 digits ISBN (optional)").optional(), fieldWithPath("[].isbn13").description("13 digits ISBN (optional)").optional(), fieldWithPath("[].title").description("Book title"), fieldWithPath("[].authors").description("Book authors (optional)").optional(), fieldWithPath("[].authors[].firstName").description("First name"), fieldWithPath("[].authors[].firstName").description("Last name"), fieldWithPath("[].pages").description("Number of pages") ))); } |
and generated documentation looks like:
Add a new book for provided user on POST operation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@PostMapping(value = "/users/{user}/books") public ResponseEntity createUserBook(@PathVariable("user") String user, @RequestBody Book book) { try { logger.debug("Add new book for user {}", user); if(hasUseTheBook(user, book)) { ErrorResponse errorResponse = new ErrorResponse(DATA_VALIDATION, asList(new ErrorCause(asList("isbn10", "isbn13"), "book.isbn.exists"))); return new ResponseEntity(errorResponse ,HttpStatus.FORBIDDEN); } String uuid = booksDao.createUserBook(user, book); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.LOCATION, String.format("/users/%s/books/%s", user, uuid)); return new ResponseEntity(httpHeaders, HttpStatus.CREATED); } catch (Exception ex) { logger.error("Error on adding new book", ex); return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); } } |
This method needs to document path parameters, field fields, response headers and response fields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
@Test public void createUserBook() throws Exception { Book book = getBook("1e4014b1-a551-4310-9f30-590c3140b695-request.json"); when(booksDao.getUserBook(JOHN_DOE_USER, book.getUuid())).thenReturn(Optional.empty()); when(booksDao.createUserBook(JOHN_DOE_USER, book)).thenReturn("1e4014b1-a551-4310-9f30-590c3140b695"); this.mockMvc.perform(post("/users/{user}/books", JOHN_DOE_USER) .content(getBookJson("1e4014b1-a551-4310-9f30-590c3140b695-request.json")) .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(status().isCreated()) .andExpect(header().string(HttpHeaders.LOCATION, "/users/" + JOHN_DOE_USER + "/books/1e4014b1-a551-4310-9f30-590c3140b695")) .andDo(document("{class-name}/{method-name}", pathParameters(parameterWithName("user").description("User id")), requestFields( fieldWithPath("isbn10").description("10 digits ISBN (optional)").optional(), fieldWithPath("isbn13").description("13 digits ISBN (optional)" ).optional(), fieldWithPath("title").description("Book title"), fieldWithPath("authors").description("Book authors (optional)").optional(), fieldWithPath("authors[].firstName").description("First name"), fieldWithPath("authors[].firstName").description("Last name"), fieldWithPath("pages").description("Number of pages") ), responseHeaders( headerWithName(HttpHeaders.LOCATION).description("New added book resource") ))); } @Test public void createExistingUserBook() throws Exception { Book book = getBook("1e4014b1-a551-4310-9f30-590c3140b695.json"); when(booksDao.getUserBooks(JOHN_DOE_USER)).thenReturn(Arrays.asList(book)); this.mockMvc.perform(post("/users/{user}/books", JOHN_DOE_USER) .content(getBookJson("1e4014b1-a551-4310-9f30-590c3140b695-request.json")) .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(status().isForbidden()) .andExpect(jsonPath("type", is("DATA_VALIDATION"))) .andExpect(jsonPath("causes[0].causes[0]", is("isbn10"))) .andExpect(jsonPath("causes[0].causes[1]", is("isbn13"))) .andExpect(jsonPath("causes[0].key", is("book.isbn.exists"))) .andDo(document("{class-name}/{method-name}", responseFields( fieldWithPath("type").description("Error type"), fieldWithPath("causes").description("Error causes"), fieldWithPath("causes[].causes") .description("Error causes (OPTIONAL). If present, it contains the name of the fields related with this error.") .optional(), fieldWithPath("causes[].key") .description("Error key. This should be used to locate the right translation for the error") ))); } |
and generated documentation looks like:
All the other resources and operations from this project are similar with these two and it does not make sense to repeat the information. I hope you found this post usefull and if you want all details, you can check the code and the entire generated documentation.