diff --git a/.vscode/settings.json b/.vscode/settings.json index 6d11e49..c7306a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,8 @@ "editor.detectIndentation": false, "editor.formatOnSave": true, "[java]": { - "editor.defaultFormatter": "redhat.java" + "editor.defaultFormatter": "redhat.java", + "editor.inlayHints.enabled": "off" }, "java.configuration.updateBuildConfiguration": "automatic", "java.compile.nullAnalysis.mode": "automatic", diff --git a/assets/images/structure.svg b/assets/images/structure.svg index d605c67..ea4f57f 100644 --- a/assets/images/structure.svg +++ b/assets/images/structure.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java index b7bcc59..e7a9da5 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java @@ -1,9 +1,13 @@ package ar.com.nanotaboada.java.samples.spring.boot.controllers; +import static org.springframework.http.HttpHeaders.LOCATION; + import java.net.URI; import java.util.List; -import org.springframework.http.HttpHeaders; +import jakarta.validation.Valid; + +import org.hibernate.validator.constraints.ISBN; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -25,6 +29,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; + import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO; import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService; @@ -52,22 +57,18 @@ public BooksController(BooksService booksService) { @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content), @ApiResponse(responseCode = "409", description = "Conflict", content = @Content) }) - public ResponseEntity post(@RequestBody BookDTO bookDTO) { - if (booksService.retrieveByIsbn(bookDTO.getIsbn()) != null) { - return new ResponseEntity<>(HttpStatus.CONFLICT); - } else { - if (booksService.create(bookDTO)) { - URI location = MvcUriComponentsBuilder - .fromMethodName(BooksController.class, "getByIsbn", bookDTO.getIsbn()) - .build() - .toUri(); - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setLocation(location); - return new ResponseEntity<>(httpHeaders, HttpStatus.CREATED); - } else { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } + public ResponseEntity post(@RequestBody @Valid BookDTO bookDTO) { + boolean created = booksService.create(bookDTO); + if (!created) { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); } + URI location = MvcUriComponentsBuilder + .fromMethodCall(MvcUriComponentsBuilder.on(BooksController.class).getByIsbn(bookDTO.getIsbn())) + .build() + .toUri(); + return ResponseEntity.status(HttpStatus.CREATED) + .header(LOCATION, location.toString()) + .build(); } /* @@ -77,18 +78,16 @@ public ResponseEntity post(@RequestBody BookDTO bookDTO) { */ @GetMapping("/books/{isbn}") - @Operation(summary = "Retrieves a book by its ID") + @Operation(summary = "Retrieves a book by its ISBN") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BookDTO.class))), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) }) public ResponseEntity getByIsbn(@PathVariable String isbn) { BookDTO bookDTO = booksService.retrieveByIsbn(isbn); - if (bookDTO != null) { - return new ResponseEntity<>(bookDTO, HttpStatus.OK); - } else { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } + return (bookDTO != null) + ? ResponseEntity.status(HttpStatus.OK).body(bookDTO) + : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } @GetMapping("/books") @@ -98,7 +97,7 @@ public ResponseEntity getByIsbn(@PathVariable String isbn) { }) public ResponseEntity> getAll() { List books = booksService.retrieveAll(); - return new ResponseEntity<>(books, HttpStatus.OK); + return ResponseEntity.status(HttpStatus.OK).body(books); } /* @@ -108,22 +107,17 @@ public ResponseEntity> getAll() { */ @PutMapping("/books") - @Operation(summary = "Updates (entirely) a book by its ID") + @Operation(summary = "Updates (entirely) a book by its ISBN") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "No Content", content = @Content), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) }) - public ResponseEntity put(@RequestBody BookDTO bookDTO) { - if (booksService.retrieveByIsbn(bookDTO.getIsbn()) != null) { - if (booksService.update(bookDTO)) { - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } else { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - } else { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } + public ResponseEntity put(@RequestBody @Valid BookDTO bookDTO) { + boolean updated = booksService.update(bookDTO); + return (updated) + ? ResponseEntity.status(HttpStatus.NO_CONTENT).build() + : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } /* @@ -133,21 +127,16 @@ public ResponseEntity put(@RequestBody BookDTO bookDTO) { */ @DeleteMapping("/books/{isbn}") - @Operation(summary = "Deletes a book by its ID") + @Operation(summary = "Deletes a book by its ISBN") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "No Content", content = @Content), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) }) - public ResponseEntity delete(@PathVariable String isbn) { - if (booksService.retrieveByIsbn(isbn) != null) { - if (booksService.delete(isbn)) { - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } else { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - } else { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } + public ResponseEntity delete(@PathVariable @ISBN String isbn) { + boolean deleted = booksService.delete(isbn); + return (deleted) + ? ResponseEntity.status(HttpStatus.NO_CONTENT).build() + : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } } diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/BooksService.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/BooksService.java index 7855537..d286ea6 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/BooksService.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/BooksService.java @@ -1,11 +1,10 @@ package ar.com.nanotaboada.java.samples.spring.boot.services; -import jakarta.validation.Validator; -import lombok.RequiredArgsConstructor; - import java.util.List; import java.util.stream.StreamSupport; +import lombok.RequiredArgsConstructor; + import org.modelmapper.ModelMapper; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; @@ -21,7 +20,6 @@ public class BooksService { private final BooksRepository booksRepository; - private final Validator validator; private final ModelMapper modelMapper; /* @@ -32,14 +30,14 @@ public class BooksService { @CachePut(value = "books", key = "#bookDTO.isbn") public boolean create(BookDTO bookDTO) { - boolean notExists = !booksRepository.existsById(bookDTO.getIsbn()); - boolean valid = validator.validate(bookDTO).isEmpty(); - if (notExists && valid) { + if (booksRepository.existsById(bookDTO.getIsbn())) { + return false; + } else { Book book = mapFrom(bookDTO); booksRepository.save(book); return true; } - return false; + } /* @@ -70,14 +68,13 @@ public List retrieveAll() { @CachePut(value = "books", key = "#bookDTO.isbn") public boolean update(BookDTO bookDTO) { - boolean exists = booksRepository.existsById(bookDTO.getIsbn()); - boolean valid = validator.validate(bookDTO).isEmpty(); - if (exists && valid) { + if (booksRepository.existsById(bookDTO.getIsbn())) { Book book = mapFrom(bookDTO); booksRepository.save(book); return true; + } else { + return false; } - return false; } /* @@ -91,8 +88,9 @@ public boolean delete(String isbn) { if (booksRepository.existsById(isbn)) { booksRepository.deleteById(isbn); return true; + } else { + return false; } - return false; } private BookDTO mapFrom(Book book) { diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOsBuilder.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOFakes.java similarity index 96% rename from src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOsBuilder.java rename to src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOFakes.java index 32b728f..d0d661a 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOsBuilder.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOFakes.java @@ -6,9 +6,13 @@ import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO; -public class BookDTOsBuilder { +public final class BookDTOFakes { - public static BookDTO buildOneValid() { + private BookDTOFakes() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static BookDTO createOneValid() { BookDTO bookDTO = new BookDTO(); bookDTO.setIsbn("978-1484200773"); bookDTO.setTitle("Pro Git"); @@ -28,7 +32,7 @@ Pro Git (Second Edition) is your fully-updated guide to Git and its \ return bookDTO; } - public static BookDTO buildOneInvalid() { + public static BookDTO createOneInvalid() { BookDTO bookDTO = new BookDTO(); bookDTO.setIsbn("978-1234567890"); // Invalid (invalid ISBN) bookDTO.setTitle("Title"); @@ -42,7 +46,7 @@ public static BookDTO buildOneInvalid() { return bookDTO; } - public static List buildManyValid() { + public static List createManyValid() { ArrayList bookDTOs = new ArrayList<>(); BookDTO bookDTO9781838986698 = new BookDTO(); bookDTO9781838986698.setIsbn("9781838986698"); diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BooksBuilder.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookFakes.java similarity index 97% rename from src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BooksBuilder.java rename to src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookFakes.java index 0cfb0f9..a71cfda 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BooksBuilder.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookFakes.java @@ -6,9 +6,13 @@ import ar.com.nanotaboada.java.samples.spring.boot.models.Book; -public class BooksBuilder { +public final class BookFakes { - public static Book buildOneValid() { + private BookFakes() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static Book createOneValid() { Book book = new Book(); book.setIsbn("9781484200773"); book.setTitle("Pro Git"); @@ -28,7 +32,7 @@ Pro Git (Second Edition) is your fully-updated guide to Git and its \ return book; } - public static Book buildOneInvalid() { + public static Book createOneInvalid() { Book book = new Book(); book.setIsbn("9781234567890"); // Invalid (invalid ISBN) book.setTitle("Title"); @@ -42,7 +46,7 @@ public static Book buildOneInvalid() { return book; } - public static List buildManyValid() { + public static List createManyValid() { ArrayList books = new ArrayList<>(); Book book9781838986698 = new Book(); book9781838986698.setIsbn("9781838986698"); diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java index 966fca7..c4e524a 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -29,7 +30,7 @@ import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO; import ar.com.nanotaboada.java.samples.spring.boot.repositories.BooksRepository; import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService; -import ar.com.nanotaboada.java.samples.spring.boot.test.BookDTOsBuilder; +import ar.com.nanotaboada.java.samples.spring.boot.test.BookDTOFakes; @DisplayName("HTTP Methods on Controller") @WebMvcTest(BooksController.class) @@ -53,17 +54,17 @@ class BooksControllerTests { */ @Test - void givenPost_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusConflict() + void givenPost_whenRequestBodyIsValidButExistingBook_thenResponseStatusIsConflict() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); - String content = new ObjectMapper().writeValueAsString(bookDTO); + BookDTO bookDTO = BookDTOFakes.createOneValid(); + String body = new ObjectMapper().writeValueAsString(bookDTO); Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(bookDTO); // Existing + .when(booksServiceMock.create(any(BookDTO.class))) + .thenReturn(false); // Existing MockHttpServletRequestBuilder request = MockMvcRequestBuilders .post(PATH) - .content(content) + .content(body) .contentType(MediaType.APPLICATION_JSON); // Act MockHttpServletResponse response = application @@ -71,25 +72,26 @@ void givenPost_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusCo .andReturn() .getResponse(); // Assert + verify(booksServiceMock, times(1)).create(any(BookDTO.class)); assertThat(response.getStatus()).isEqualTo(HttpStatus.CONFLICT.value()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); + } @Test - void givenPost_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusCreatedAndLocationHeader() + void givenPost_whenRequestBodyIsValidAndNonExistentBook_thenResponseStatusIsCreated() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); - String content = new ObjectMapper().writeValueAsString(bookDTO); + BookDTO bookDTO = BookDTOFakes.createOneValid(); + String body = new ObjectMapper().writeValueAsString(bookDTO); Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(null); // New + .when(booksServiceMock.create(any(BookDTO.class))) + .thenReturn(true); // Non-existent Mockito .when(booksServiceMock.create(any(BookDTO.class))) .thenReturn(true); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .post(PATH) - .content(content) + .content(body) .contentType(MediaType.APPLICATION_JSON); // Act MockHttpServletResponse response = application @@ -98,28 +100,22 @@ void givenPost_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusCreated .getResponse(); response.setContentType("application/json;charset=UTF-8"); // Assert + verify(booksServiceMock, times(1)).create(any(BookDTO.class)); assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value()); assertThat(response.getHeader(HttpHeaders.LOCATION)).isNotNull(); assertThat(response.getHeader(HttpHeaders.LOCATION)).contains(PATH + "/" + bookDTO.getIsbn()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); - verify(booksServiceMock, times(1)).create(any(BookDTO.class)); + } @Test - void givenPost_whenRequestBodyContainsInvalidBook_thenShouldReturnStatusBadRequest() + void givenPost_whenRequestBodyIsInvalidBook_thenResponseStatusIsBadRequest() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); - String content = new ObjectMapper().writeValueAsString(bookDTO); - Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(null); // New - Mockito - .when(booksServiceMock.create(any(BookDTO.class))) - .thenReturn(false); + BookDTO bookDTO = BookDTOFakes.createOneInvalid(); + String body = new ObjectMapper().writeValueAsString(bookDTO); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .post(PATH) - .content(content) + .content(body) .contentType(MediaType.APPLICATION_JSON); // Act MockHttpServletResponse response = application @@ -127,9 +123,8 @@ void givenPost_whenRequestBodyContainsInvalidBook_thenShouldReturnStatusBadReque .andReturn() .getResponse(); // Assert + verify(booksServiceMock, never()).create(any(BookDTO.class)); assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); - verify(booksServiceMock, times(1)).create(any(BookDTO.class)); } /* @@ -139,15 +134,16 @@ void givenPost_whenRequestBodyContainsInvalidBook_thenShouldReturnStatusBadReque */ @Test - void givenGetByIsbn_whenRequestParameterIdentifiesExistingBook_thenShouldReturnStatusOkAndTheBook() + void givenGetByIsbn_whenRequestPathVariableIsValidAndExistingISBN_thenResponseStatusIsOKAndResultIsBook() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); + BookDTO bookDTO = BookDTOFakes.createOneValid(); + String isbn = bookDTO.getIsbn(); Mockito .when(booksServiceMock.retrieveByIsbn(anyString())) .thenReturn(bookDTO); // Existing MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH + "/{isbn}", bookDTO.getIsbn()); + .get(PATH + "/{isbn}", isbn); // Act MockHttpServletResponse response = application .perform(request) @@ -155,21 +151,21 @@ void givenGetByIsbn_whenRequestParameterIdentifiesExistingBook_thenShouldReturnS .getResponse(); response.setContentType("application/json;charset=UTF-8"); String content = response.getContentAsString(); - BookDTO actual = new ObjectMapper().readValue(content, BookDTO.class); + BookDTO result = new ObjectMapper().readValue(content, BookDTO.class); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - assertThat(actual).usingRecursiveComparison().isEqualTo(bookDTO); verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(result).usingRecursiveComparison().isEqualTo(bookDTO); } @Test - void givenGetByIsbn_whenRequestParameterDoesNotIdentifyAnExistingBook_thenShouldReturnStatusNotFound() + void givenGetByIsbn_whenRequestPathVariableIsValidButNonExistentISBN_thenResponseStatusIsNotFound() throws Exception { // Arrange String isbn = "9781484242216"; Mockito .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(null); // New + .thenReturn(null); // Non-existent MockHttpServletRequestBuilder request = MockMvcRequestBuilders .get(PATH + "/{isbn}", isbn); // Act @@ -178,18 +174,18 @@ void givenGetByIsbn_whenRequestParameterDoesNotIdentifyAnExistingBook_thenShould .andReturn() .getResponse(); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @Test - void givenGetAll_whenRequestPathIsBooks_thenShouldReturnStatusOkAndCollectionOfBooks() + void givenGetAll_whenRequestPathIsBooks_thenResponseIsOkAndResultIsBooks() throws Exception { // Arrange - List expected = BookDTOsBuilder.buildManyValid(); + List bookDTOs = BookDTOFakes.createManyValid(); Mockito .when(booksServiceMock.retrieveAll()) - .thenReturn(expected); + .thenReturn(bookDTOs); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .get(PATH); // Act @@ -199,12 +195,12 @@ void givenGetAll_whenRequestPathIsBooks_thenShouldReturnStatusOkAndCollectionOfB .getResponse(); response.setContentType("application/json;charset=UTF-8"); String content = response.getContentAsString(); - List actual = new ObjectMapper().readValue(content, new TypeReference>() { + List result = new ObjectMapper().readValue(content, new TypeReference>() { }); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - assertThat(actual).usingRecursiveComparison().isEqualTo(expected); verify(booksServiceMock, times(1)).retrieveAll(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(result).usingRecursiveComparison().isEqualTo(bookDTOs); } /* @@ -214,20 +210,17 @@ void givenGetAll_whenRequestPathIsBooks_thenShouldReturnStatusOkAndCollectionOfB */ @Test - void givenPut_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusNoContent() + void givenPut_whenRequestBodyIsValidAndExistingBook_thenResponseStatusIsNoContent() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); - String content = new ObjectMapper().writeValueAsString(bookDTO); - Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(bookDTO); // Existing + BookDTO bookDTO = BookDTOFakes.createOneValid(); + String body = new ObjectMapper().writeValueAsString(bookDTO); Mockito .when(booksServiceMock.update(any(BookDTO.class))) - .thenReturn(true); + .thenReturn(true); // Existing MockHttpServletRequestBuilder request = MockMvcRequestBuilders .put(PATH) - .content(content) + .content(body) .contentType(MediaType.APPLICATION_JSON); // Act MockHttpServletResponse response = application @@ -235,26 +228,22 @@ void givenPut_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusNoC .andReturn() .getResponse(); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); verify(booksServiceMock, times(1)).update(any(BookDTO.class)); + assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); } @Test - void givenPut_whenRequestBodyContainsExistingInvalidBook_thenShouldReturnStatusBadRequest() + void givenPut_whenRequestBodyIsValidButNonExistentBook_thenResponseStatusIsNotFound() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); - String content = new ObjectMapper().writeValueAsString(bookDTO); - Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(bookDTO); // Existing + BookDTO bookDTO = BookDTOFakes.createOneValid(); + String body = new ObjectMapper().writeValueAsString(bookDTO); Mockito .when(booksServiceMock.update(any(BookDTO.class))) - .thenReturn(false); + .thenReturn(false); // Non-existent MockHttpServletRequestBuilder request = MockMvcRequestBuilders .put(PATH) - .content(content) + .content(body) .contentType(MediaType.APPLICATION_JSON); // Act MockHttpServletResponse response = application @@ -262,23 +251,19 @@ void givenPut_whenRequestBodyContainsExistingInvalidBook_thenShouldReturnStatusB .andReturn() .getResponse(); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); verify(booksServiceMock, times(1)).update(any(BookDTO.class)); + assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @Test - void givenPut_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusNotFound() + void givenPut_whenRequestBodyIsInvalidBook_thenResponseStatusIsBadRequest() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); - String content = new ObjectMapper().writeValueAsString(bookDTO); - Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(null); // New + BookDTO bookDTO = BookDTOFakes.createOneInvalid(); + String body = new ObjectMapper().writeValueAsString(bookDTO); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .put(PATH) - .content(content) + .content(body) .contentType(MediaType.APPLICATION_JSON); // Act MockHttpServletResponse response = application @@ -286,8 +271,8 @@ void givenPut_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusNotFound .andReturn() .getResponse(); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); + verify(booksServiceMock, never()).update(any(BookDTO.class)); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } /* @@ -297,70 +282,62 @@ void givenPut_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusNotFound */ @Test - void givenDelete_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusNoContent() + void givenDelete_whenPathVariableIsValidAndExistingISBN_thenResponseStatusIsNoContent() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); - Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(bookDTO); // Existing + BookDTO bookDTO = BookDTOFakes.createOneValid(); + String isbn = bookDTO.getIsbn(); Mockito .when(booksServiceMock.delete(anyString())) - .thenReturn(true); + .thenReturn(true); // Existing MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete(PATH + "/{isbn}", bookDTO.getIsbn()); + .delete(PATH + "/{isbn}", isbn); // Act MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); verify(booksServiceMock, times(1)).delete(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); } @Test - void givenDelete_whenRequestBodyContainsExistingInvalidBook_thenShouldReturnStatusBadRequest() + void givenDelete_whenPathVariableIsValidButNonExistentISBN_thenResponseStatusIsNotFound() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); - Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(bookDTO); // Existing + BookDTO bookDTO = BookDTOFakes.createOneValid(); + String isbn = bookDTO.getIsbn(); Mockito .when(booksServiceMock.delete(anyString())) - .thenReturn(false); + .thenReturn(false); // Non-existent MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete(PATH + "/{isbn}", bookDTO.getIsbn()); + .delete(PATH + "/{isbn}", isbn); // Act MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); verify(booksServiceMock, times(1)).delete(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @Test - void givenDelete_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusNotFound() + void givenDelete_whenPathVariableIsInvalidISBN_thenResponseStatusIsBadRequest() throws Exception { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); - Mockito - .when(booksServiceMock.retrieveByIsbn(anyString())) - .thenReturn(null); // New + BookDTO bookDTO = BookDTOFakes.createOneInvalid(); + String isbn = bookDTO.getIsbn(); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete(PATH + "/{isbn}", bookDTO.getIsbn()); + .delete(PATH + "/{isbn}", isbn); // Act MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); // Assert - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); - verify(booksServiceMock, times(1)).retrieveByIsbn(anyString()); + verify(booksServiceMock, never()).delete(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } } diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java index 7b312d5..47c5ac7 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java @@ -12,7 +12,7 @@ import ar.com.nanotaboada.java.samples.spring.boot.models.Book; import ar.com.nanotaboada.java.samples.spring.boot.repositories.BooksRepository; -import ar.com.nanotaboada.java.samples.spring.boot.test.BooksBuilder; +import ar.com.nanotaboada.java.samples.spring.boot.test.BookFakes; @DisplayName("Derived Query Methods on Repository") @DataJpaTest @@ -22,9 +22,9 @@ class BooksRepositoryTests { private BooksRepository repository; @Test - void givenFindByIsbn_whenIsbnAlreadyExists_thenShouldReturnExistingBook() { + void givenFindByIsbn_whenISBNAlreadyExists_thenShouldReturnExistingBook() { // Arrange - Book existing = BooksBuilder.buildOneValid(); + Book existing = BookFakes.createOneValid(); repository.save(existing); // Exists // Act Optional actual = repository.findByIsbn(existing.getIsbn()); @@ -34,9 +34,9 @@ void givenFindByIsbn_whenIsbnAlreadyExists_thenShouldReturnExistingBook() { } @Test - void givenFindByIsbn_whenIsbnDoesNotExist_thenShouldReturnEmptyOptional() { + void givenFindByIsbn_whenISBNDoesNotExist_thenShouldReturnEmptyOptional() { // Arrange - Book expected = BooksBuilder.buildOneValid(); + Book expected = BookFakes.createOneValid(); // Act Optional actual = repository.findByIsbn(expected.getIsbn()); // Assert diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java index 70985d4..3e62ba7 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java @@ -3,18 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.util.HashSet; import java.util.List; import java.util.Optional; -import java.util.Set; - -import jakarta.validation.ConstraintViolation; -import jakarta.validation.Validator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,8 +23,8 @@ import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO; import ar.com.nanotaboada.java.samples.spring.boot.repositories.BooksRepository; import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService; -import ar.com.nanotaboada.java.samples.spring.boot.test.BookDTOsBuilder; -import ar.com.nanotaboada.java.samples.spring.boot.test.BooksBuilder; +import ar.com.nanotaboada.java.samples.spring.boot.test.BookDTOFakes; +import ar.com.nanotaboada.java.samples.spring.boot.test.BookFakes; @DisplayName("CRUD Operations on Service") @ExtendWith(MockitoExtension.class) @@ -39,9 +33,6 @@ class BooksServiceTests { @Mock private BooksRepository booksRepositoryMock; - @Mock - private Validator validatorMock; - @Mock private ModelMapper modelMapperMock; @@ -55,86 +46,39 @@ class BooksServiceTests { */ @Test - void givenCreate_whenBookExistsInRepositoryAndIsValid_thenShouldNeverSaveBookIntoRepositoryAndReturnFalse() { + void givenCreate_whenRepositoryExistsByIdReturnsTrue_thenRepositoryNeverSaveBookAndResultIsFalse() { // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); + BookDTO bookDTO = BookDTOFakes.createOneInvalid(); Mockito .when(booksRepositoryMock.existsById(anyString())) .thenReturn(true); - Mockito - .when(validatorMock.validate(any(BookDTO.class))) - .thenReturn(new HashSet>()); // Act - result = booksService.create(bookDTO); + boolean result = booksService.create(bookDTO); // Assert verify(booksRepositoryMock, never()).save(any(Book.class)); + verify(modelMapperMock, never()).map(bookDTO, Book.class); assertThat(result).isFalse(); } @Test - void givenCreate_whenBookExistsInRepositoryAndIsNotValid_thenShouldNeverSaveBookIntoRepositoryAndReturnFalse() { + void givenCreate_whenRepositoryExistsByIdReturnsFalse_thenRepositorySaveBookAndResultIsTrue() { // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); - Mockito - .when(booksRepositoryMock.existsById(anyString())) - .thenReturn(true); - @SuppressWarnings("unchecked") - Set> constraintViolations = Set.of(mock(ConstraintViolation.class)); - Mockito - .when(validatorMock.validate(any(BookDTO.class))) - .thenReturn(constraintViolations); - // Act - result = booksService.create(bookDTO); - // Assert - verify(booksRepositoryMock, never()).save(any(Book.class)); - assertThat(result).isFalse(); - } - - @Test - void givenCreate_whenBookdDoesNotExistsInRepositoryAndIsValid_thenShouldSaveBookIntoRepositoryAndReturnTrue() { - // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); - Book book = BooksBuilder.buildOneValid(); + Book book = BookFakes.createOneValid(); + BookDTO bookDTO = BookDTOFakes.createOneValid(); Mockito .when(booksRepositoryMock.existsById(anyString())) .thenReturn(false); - Mockito - .when(validatorMock.validate(any(BookDTO.class))) - .thenReturn(new HashSet>()); Mockito .when(modelMapperMock.map(bookDTO, Book.class)) .thenReturn(book); // Act - result = booksService.create(bookDTO); + boolean result = booksService.create(bookDTO); // Assert - verify(modelMapperMock, times(1)).map(bookDTO, Book.class); verify(booksRepositoryMock, times(1)).save(any(Book.class)); + verify(modelMapperMock, times(1)).map(bookDTO, Book.class); assertThat(result).isTrue(); } - @Test - void givenCreate_whenBookDoesNotExistsInRepositoryAndIsNotValid_thenShouldNeverSaveBookIntoRepositoryAndReturnFalse() { - // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); - Mockito - .when(booksRepositoryMock.existsById(anyString())) - .thenReturn(false); - @SuppressWarnings("unchecked") - Set> constraintViolations = Set.of(mock(ConstraintViolation.class)); - Mockito - .when(validatorMock.validate(any(BookDTO.class))) - .thenReturn(constraintViolations); - // Act - result = booksService.create(bookDTO); - // Assert - verify(booksRepositoryMock, never()).save(any(Book.class)); - assertThat(result).isFalse(); - } - /* * ------------------------------------------------------------------------- * Retrieve @@ -142,10 +86,10 @@ void givenCreate_whenBookDoesNotExistsInRepositoryAndIsNotValid_thenShouldNeverS */ @Test - void givenRetrieveByIsbn_whenIsbnIsFoundInRepository_thenShouldReturnBook() { + void givenRetrieveByIsbn_whenRepositoryFindByIdReturnsBook_thenResultIsEqualToBook() { // Arrange - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); - Book book = BooksBuilder.buildOneValid(); + Book book = BookFakes.createOneValid(); + BookDTO bookDTO = BookDTOFakes.createOneValid(); Mockito .when(booksRepositoryMock.findByIsbn(anyString())) .thenReturn(Optional.of(book)); @@ -155,13 +99,13 @@ void givenRetrieveByIsbn_whenIsbnIsFoundInRepository_thenShouldReturnBook() { // Act BookDTO result = booksService.retrieveByIsbn(bookDTO.getIsbn()); // Assert + verify(booksRepositoryMock, times(1)).findByIsbn(anyString()); verify(modelMapperMock, times(1)).map(book, BookDTO.class); assertThat(result).usingRecursiveComparison().isEqualTo(bookDTO); - verify(booksRepositoryMock, times(1)).findByIsbn(anyString()); } @Test - void givenRetrieveByIsbn_whenIsbnIsNotFoundInRepository_thenShouldReturnNull() { + void givenRetrieveByIsbn_whenRepositoryFindByIdReturnsEmpty_thenResultIsNull() { // Arrange String isbn = "9781484242216"; Mockito @@ -170,32 +114,32 @@ void givenRetrieveByIsbn_whenIsbnIsNotFoundInRepository_thenShouldReturnNull() { // Act BookDTO result = booksService.retrieveByIsbn(isbn); // Assert - assertThat(result).isNull(); - verify(modelMapperMock, never()).map(any(Book.class), any(BookDTO.class)); verify(booksRepositoryMock, times(1)).findByIsbn(anyString()); + verify(modelMapperMock, never()).map(any(Book.class), any(BookDTO.class)); + assertThat(result).isNull(); } @Test - void givenRetrieveAll_whenRepositoryHasCollectionOfBooks_thenShouldReturnAllBooks() { + void givenRetrieveAll_whenRepositoryFindAllReturnsBooks_thenResultIsEqualToBooks() { // Arrange - List expected = BookDTOsBuilder.buildManyValid(); - List existing = BooksBuilder.buildManyValid(); + List books = BookFakes.createManyValid(); + List bookDTOs = BookDTOFakes.createManyValid(); Mockito .when(booksRepositoryMock.findAll()) - .thenReturn(existing); - for (int index = 0; index < existing.size(); index++) { + .thenReturn(books); + for (int index = 0; index < books.size(); index++) { Mockito - .when(modelMapperMock.map(existing.get(index), BookDTO.class)) - .thenReturn(expected.get(index)); + .when(modelMapperMock.map(books.get(index), BookDTO.class)) + .thenReturn(bookDTOs.get(index)); } // Act - List actual = booksService.retrieveAll(); + List result = booksService.retrieveAll(); // Assert - assertThat(actual).usingRecursiveComparison().isEqualTo(expected); - for (Book book : existing) { + verify(booksRepositoryMock, times(1)).findAll(); + for (Book book : books) { verify(modelMapperMock, times(1)).map(book, BookDTO.class); } - verify(booksRepositoryMock, times(1)).findAll(); + assertThat(result).usingRecursiveComparison().isEqualTo(bookDTOs); } /* @@ -205,83 +149,37 @@ void givenRetrieveAll_whenRepositoryHasCollectionOfBooks_thenShouldReturnAllBook */ @Test - void givenUpdate_whenBookExistsInRepositoryAndIsValid_thenShouldSaveBookIntoRepositoryAndReturnTrue() { + void givenUpdate_whenRepositoryExistsByIdReturnsTrue_thenRepositorySaveBookAndResultIsTrue() { // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); - Book book = BooksBuilder.buildOneValid(); + Book book = BookFakes.createOneValid(); + BookDTO bookDTO = BookDTOFakes.createOneValid(); + Mockito .when(booksRepositoryMock.existsById(anyString())) .thenReturn(true); - Mockito - .when(validatorMock.validate(any(BookDTO.class))) - .thenReturn(new HashSet>()); Mockito .when(modelMapperMock.map(bookDTO, Book.class)) .thenReturn(book); // Act - result = booksService.update(bookDTO); + boolean result = booksService.update(bookDTO); // Assert - verify(modelMapperMock, times(1)).map(bookDTO, Book.class); verify(booksRepositoryMock, times(1)).save(any(Book.class)); + verify(modelMapperMock, times(1)).map(bookDTO, Book.class); assertThat(result).isTrue(); } @Test - void givenUodate_whenBookExistsInRepositoryAndIsNotValid_thenShouldNeverSaveBookIntoRepositoryAndReturnFalse() { - // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); - Mockito - .when(booksRepositoryMock.existsById(anyString())) - .thenReturn(true); - @SuppressWarnings("unchecked") - Set> constraintViolations = Set.of(mock(ConstraintViolation.class)); - Mockito - .when(validatorMock.validate(any(BookDTO.class))) - .thenReturn(constraintViolations); - // Act - result = booksService.update(bookDTO); - // Assert - verify(booksRepositoryMock, never()).save(any(Book.class)); - assertThat(result).isFalse(); - } - - @Test - void givenUpdate_whenBookDoesNotExistsInRepositoryAndIsValid_thenShouldNeverSaveBookIntoRepositoryAndReturnFalse() { - // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); - Mockito - .when(booksRepositoryMock.existsById(anyString())) - .thenReturn(false); - Mockito - .when(validatorMock.validate(any(BookDTO.class))) - .thenReturn(new HashSet>()); - // Act - result = booksService.update(bookDTO); - // Assert - verify(booksRepositoryMock, never()).save(any(Book.class)); - assertThat(result).isFalse(); - } - - @Test - void givenUodate_whenBookDoesNotExistsInRepositoryAndIsNotValid_thenShouldNeverSaveBookIntoRepositoryAndReturnFalse() { + void givenUpdate_whenRepositoryExistsByIdReturnsFalse_thenRepositoryNeverSaveBookAndResultIsFalse() { // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); + BookDTO bookDTO = BookDTOFakes.createOneValid(); Mockito .when(booksRepositoryMock.existsById(anyString())) .thenReturn(false); - @SuppressWarnings("unchecked") - Set> constraintViolations = Set.of(mock(ConstraintViolation.class)); - Mockito - .when(validatorMock.validate(any(BookDTO.class))) - .thenReturn(constraintViolations); // Act - result = booksService.update(bookDTO); + boolean result = booksService.update(bookDTO); // Assert verify(booksRepositoryMock, never()).save(any(Book.class)); + verify(modelMapperMock, never()).map(bookDTO, Book.class); assertThat(result).isFalse(); } @@ -292,45 +190,32 @@ void givenUodate_whenBookDoesNotExistsInRepositoryAndIsNotValid_thenShouldNeverS */ @Test - void givenDelete_whenIsbnIsBlank_thenShouldNeverDeleteBookFromRepositoryAndReturnFalse() { - // Arrange - boolean result = false; - Book book = BooksBuilder.buildOneValid(); - book.setIsbn(""); - // Act - result = booksService.delete(book.getIsbn()); - // Assert - verify(booksRepositoryMock, never()).deleteById(anyString()); - assertThat(result).isFalse(); - } - - @Test - void givenDelete_whenIsbnIsNotBlankButDoesNotExistInRepository_thenShouldNeverDeleteBookFromRepositoryAndReturnFalse() { + void givenDelete_whenRepositoryExistsByIdReturnsFalse_thenRepositoryNeverDeleteBookAndResultIsFalse() { // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid(); + BookDTO bookDTO = BookDTOFakes.createOneInvalid(); Mockito .when(booksRepositoryMock.existsById(anyString())) .thenReturn(false); // Act - result = booksService.delete(bookDTO.getIsbn()); + boolean result = booksService.delete(bookDTO.getIsbn()); // Assert verify(booksRepositoryMock, never()).deleteById(anyString()); + verify(modelMapperMock, never()).map(bookDTO, Book.class); assertThat(result).isFalse(); } @Test - void givenDelete_whenIsbnIsNotBlankAndExistInRepository_thenShouldDeleteBookFromRepositoryAndReturnTrue() { + void givenDelete_whenRepositoryExistsByIdReturnsTrue_thenRepositoryDeleteBookAndResultIsTrue() { // Arrange - boolean result = false; - BookDTO bookDTO = BookDTOsBuilder.buildOneValid(); + BookDTO bookDTO = BookDTOFakes.createOneValid(); Mockito .when(booksRepositoryMock.existsById(anyString())) .thenReturn(true); // Act - result = booksService.delete(bookDTO.getIsbn()); + boolean result = booksService.delete(bookDTO.getIsbn()); // Assert verify(booksRepositoryMock, times(1)).deleteById(anyString()); + verify(modelMapperMock, never()).map(bookDTO, Book.class); assertThat(result).isTrue(); } }