diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 2c072f6..df0c121 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -84,7 +84,7 @@ reviews: - Test classes should use JUnit 5 (Jupiter) - Verify proper Spring test annotations (@WebMvcTest, @DataJpaTest, etc.) - Check use of @MockitoBean (Spring Boot 4.0 style) - - Ensure test naming follows method_scenario_outcome pattern (concise) + - Ensure test naming follows givenX_whenY_thenZ BDD pattern - Validate BDD semantics in JavaDoc comments (Given/When/Then) - Validate use of AssertJ for fluent assertions - Check @DisplayName for readable test descriptions diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8138ac3..3befb12 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -43,7 +43,7 @@ http://localhost:9001/actuator/health # Health check - **Validation**: Bean Validation annotations in DTOs (`@NotNull`, `@Min`, etc.) - **Caching**: Spring `@Cacheable` on service layer (1-hour TTL) - **DTO Pattern**: ModelMapper for entity ↔ DTO transformations -- **Repository**: Spring Data JPA with derived queries and custom JPQL +- **Repository**: Spring Data JPA with derived queries (prefer over custom JPQL) ## Code Conventions @@ -67,21 +67,21 @@ http://localhost:9001/actuator/health # Health check ## Testing - **Structure**: `*Tests.java` in `src/test/java/` (mirrors main package structure) -- **Naming Pattern**: `method_scenario_outcome` - - `method`: Method under test (e.g., `post`, `findById`, `create`) - - `scenario`: Context (e.g., `playerExists`, `invalidData`, `noMatches`) - - `outcome`: Expected result (e.g., `returnsPlayer`, `returnsConflict`, `returnsEmpty`) +- **Naming Pattern**: `givenX_whenY_thenZ` (BDD Given-When-Then) + - `given`: Preconditions/context (e.g., `givenPlayerExists`, `givenInvalidData`, `givenNoMatches`) + - `when`: Action being tested (e.g., `whenPost`, `whenFindById`, `whenCreate`) + - `then`: Expected outcome (e.g., `thenReturnsPlayer`, `thenReturnsConflict`, `thenReturnsEmpty`) - **Examples**: ```java // Controller - void post_squadNumberExists_returnsConflict() + void givenSquadNumberExists_whenPost_thenReturnsConflict() // Service - void create_noConflict_returnsPlayerDTO() + void givenNoConflict_whenCreate_thenReturnsPlayerDTO() // Repository - void findById_playerExists_returnsPlayer() + void givenPlayerExists_whenFindById_thenReturnsPlayer() ``` - **JavaDoc**: BDD Given/When/Then structure in test comments @@ -97,7 +97,7 @@ http://localhost:9001/actuator/health # Health check ``` - **Annotations**: `@SpringBootTest`, `@AutoConfigureMockMvc`, `@Test` -- **Assertions**: AssertJ (fluent, e.g., `assertThat(result).isNotNull()`) +- **Assertions**: AssertJ BDD style (e.g., `then(result).isNotNull()`) - **Mocking**: Mockito for service layer tests - **Database**: Tests use in-memory SQLite (auto-cleared after each test) - **Coverage**: Target high coverage (JaCoCo reports in `target/site/jacoco/`) diff --git a/README.md b/README.md index 3760bc5..b51f410 100644 --- a/README.md +++ b/README.md @@ -243,10 +243,10 @@ open target/site/jacoco/index.html - **Test Database** - SQLite in-memory (jdbc:sqlite::memory:) for fast, isolated test execution - **Mocking** - Mockito with `@MockitoBean` for dependency mocking - **Assertions** - AssertJ fluent assertions -- **Naming Convention** - `method_scenario_outcome` pattern: - - `getAll_playersExist_returnsOkWithAllPlayers()` - - `post_squadNumberExists_returnsConflict()` - - `findById_playerExists_returnsPlayer()` +- **Naming Convention** - `givenX_whenY_thenZ` BDD pattern: + - `givenPlayersExist_whenGetAll_thenReturnsOkWithAllPlayers()` + - `givenSquadNumberExists_whenPost_thenReturnsConflict()` + - `givenPlayerExists_whenFindById_thenReturnsPlayer()` **Coverage Targets:** diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java index 267cd85..90f6418 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java @@ -39,7 +39,7 @@ *
  • GET {@code /players} - Retrieve all players
  • *
  • GET {@code /players/{id}} - Retrieve player by ID
  • *
  • GET {@code /players/search/league/{league}} - Search players by league name
  • - *
  • GET {@code /players/search/squadnumber/{number}} - Search player by squad number
  • + *
  • GET {@code /players/squadnumber/{number}} - Retrieve player by squad number
  • *
  • POST {@code /players} - Create a new player
  • *
  • PUT {@code /players/{id}} - Update an existing player
  • *
  • DELETE {@code /players/{id}} - Delete a player by ID
  • @@ -113,6 +113,24 @@ public ResponseEntity post(@RequestBody @Valid PlayerDTO playerDTO) { * ----------------------------------------------------------------------------------------------------------------------- */ + /** + * Retrieves all players in the squad. + *

    + * Returns the complete Argentina 2022 FIFA World Cup squad (26 players). + *

    + * + * @return 200 OK with array of all players (empty array if none found) + */ + @GetMapping("/players") + @Operation(summary = "Retrieves all players") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO[].class))) + }) + public ResponseEntity> getAll() { + List players = playersService.retrieveAll(); + return ResponseEntity.status(HttpStatus.OK).body(players); + } + /** * Retrieves a single player by their unique identifier. * @@ -133,21 +151,26 @@ public ResponseEntity getById(@PathVariable Long id) { } /** - * Retrieves all players in the squad. + * Retrieves a player by their squad number (unique identifier). *

    - * Returns the complete Argentina 2022 FIFA World Cup squad (26 players). + * Squad numbers are unique jersey numbers (e.g., Messi is #10). This is a direct lookup similar to getById(). + * Example: {@code /players/squadnumber/10} returns Lionel Messi *

    * - * @return 200 OK with array of all players (empty array if none found) + * @param squadNumber the squad number to retrieve (jersey number, typically 1-99) + * @return 200 OK with player data, or 404 Not Found if no player has that number */ - @GetMapping("/players") - @Operation(summary = "Retrieves all players") + @GetMapping("/players/squadnumber/{squadNumber}") + @Operation(summary = "Retrieves a player by squad number") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO[].class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO.class))), + @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) }) - public ResponseEntity> getAll() { - List players = playersService.retrieveAll(); - return ResponseEntity.status(HttpStatus.OK).body(players); + public ResponseEntity getBySquadNumber(@PathVariable Integer squadNumber) { + PlayerDTO player = playersService.retrieveBySquadNumber(squadNumber); + return (player != null) + ? ResponseEntity.status(HttpStatus.OK).body(player) + : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } /** @@ -169,30 +192,6 @@ public ResponseEntity> searchByLeague(@PathVariable String leagu return ResponseEntity.status(HttpStatus.OK).body(players); } - /** - * Searches for a player by their squad number. - *

    - * Squad numbers are jersey numbers that users recognize (e.g., Messi is #10). - * Example: {@code /players/search/squadnumber/10} returns Lionel Messi - *

    - * - * @param squadNumber the squad number to search for (jersey number, typically 1-99) - * @return 200 OK with player data, or 404 Not Found if no player has that - * number - */ - @GetMapping("/players/search/squadnumber/{squadNumber}") - @Operation(summary = "Searches for a player by squad number") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO.class))), - @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) - }) - public ResponseEntity searchBySquadNumber(@PathVariable Integer squadNumber) { - PlayerDTO player = playersService.searchBySquadNumber(squadNumber); - return (player != null) - ? ResponseEntity.status(HttpStatus.OK).body(player) - : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - /* * ----------------------------------------------------------------------------------------------------------------------- * HTTP PUT diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java index 87cb125..34a281b 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java @@ -4,8 +4,6 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ar.com.nanotaboada.java.samples.spring.boot.models.Player; @@ -26,8 +24,8 @@ * *

    Query Strategies:

    *
      - *
    • @Query: Explicit JPQL for complex searches (findByLeagueContainingIgnoreCase)
    • - *
    • Method Names: Spring Data derives queries from method names (Query Creation)
    • + *
    • Derived Queries: Spring Data derives queries from method names (findBySquadNumber, + * findByLeagueContainingIgnoreCase)
    • *
    * * @see Player @@ -40,20 +38,6 @@ @Repository public interface PlayersRepository extends JpaRepository { - /** - * Finds players by league name using case-insensitive wildcard matching. - *

    - * This method uses a custom JPQL query with LIKE operator for partial matches. - * For example, searching for "Premier" will match "Premier League". - *

    - * - * @param league the league name to search for (partial matches allowed) - * @return a list of players whose league name contains the search term (empty - * list if none found) - */ - @Query("SELECT p FROM Player p WHERE LOWER(p.league) LIKE LOWER(CONCAT('%', :league, '%'))") - List findByLeagueContainingIgnoreCase(@Param("league") String league); - /** * Finds a player by their squad number (exact match). *

    @@ -67,4 +51,17 @@ public interface PlayersRepository extends JpaRepository { * @return an Optional containing the player if found, empty Optional otherwise */ Optional findBySquadNumber(Integer squadNumber); + + /** + * Finds players by league name using case-insensitive wildcard matching. + *

    + * This method uses Spring Data's derived query mechanism to perform partial matching. + * For example, searching for "Premier" will match "Premier League". + *

    + * + * @param league the league name to search for (partial matches allowed) + * @return a list of players whose league name contains the search term (empty + * list if none found) + */ + List findByLeagueContainingIgnoreCase(String league); } diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java index 3e3366e..763b501 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java @@ -110,6 +110,24 @@ public PlayerDTO create(PlayerDTO playerDTO) { * ----------------------------------------------------------------------------------------------------------------------- */ + /** + * Retrieves all players from the database. + *

    + * This method returns the complete Argentina 2022 FIFA World Cup squad (26 players). + * Results are cached to improve performance on subsequent calls. + *

    + * + * @return a list of all players (empty list if none found) + * @see org.springframework.cache.annotation.Cacheable + */ + @Cacheable(value = "players") + public List retrieveAll() { + return playersRepository.findAll() + .stream() + .map(this::mapFrom) + .toList(); + } + /** * Retrieves a player by their unique identifier. *

    @@ -130,21 +148,21 @@ public PlayerDTO retrieveById(Long id) { } /** - * Retrieves all players from the database. + * Retrieves a player by their squad number (unique identifier). *

    - * This method returns the complete Argentina 2022 FIFA World Cup squad (26 players). - * Results are cached to improve performance on subsequent calls. + * Squad numbers are unique jersey numbers (e.g., Messi is #10). This is a direct lookup by unique identifier, + * similar to retrieveById(). Results are cached to improve performance. *

    * - * @return a list of all players (empty list if none found) + * @param squadNumber the squad number to retrieve (jersey number, typically 1-99) + * @return the player DTO if found, null otherwise * @see org.springframework.cache.annotation.Cacheable */ - @Cacheable(value = "players") - public List retrieveAll() { - return playersRepository.findAll() - .stream() + @Cacheable(value = "players", key = "'squad-' + #squadNumber", unless = "#result == null") + public PlayerDTO retrieveBySquadNumber(Integer squadNumber) { + return playersRepository.findBySquadNumber(squadNumber) .map(this::mapFrom) - .toList(); + .orElse(null); } /* @@ -170,22 +188,6 @@ public List searchByLeague(String league) { .toList(); } - /** - * Searches for a player by their squad number. - *

    - * This method performs an exact match on the squad number field. Squad numbers are jersey numbers that users recognize - * (e.g., Messi is #10). - *

    - * - * @param squadNumber the squad number to search for (jersey number, typically 1-99) - * @return the player DTO if found, null otherwise - */ - public PlayerDTO searchBySquadNumber(Integer squadNumber) { - return playersRepository.findBySquadNumber(squadNumber) - .map(this::mapFrom) - .orElse(null); - } - /* * ----------------------------------------------------------------------------------------------------------------------- * Update diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java index cc64250..a31f1f6 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java @@ -1,6 +1,6 @@ package ar.com.nanotaboada.java.samples.spring.boot.test.controllers; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.BDDAssertions.then; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.never; @@ -32,7 +32,6 @@ import ar.com.nanotaboada.java.samples.spring.boot.controllers.PlayersController; import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerDTO; -import ar.com.nanotaboada.java.samples.spring.boot.repositories.PlayersRepository; import ar.com.nanotaboada.java.samples.spring.boot.services.PlayersService; import ar.com.nanotaboada.java.samples.spring.boot.test.PlayerDTOFakes; @@ -50,9 +49,6 @@ class PlayersControllerTests { @MockitoBean private PlayersService playersServiceMock; - @MockitoBean - private PlayersRepository playersRepositoryMock; - @Autowired private ObjectMapper objectMapper; @@ -72,87 +68,87 @@ public ObjectMapper objectMapper() { /** * Given valid player data is provided - * When POST /players is called and the service successfully creates the player + * When creating a new player * Then response status is 201 Created and Location header points to the new resource */ @Test - void post_validPlayer_returnsCreated() + void givenValidPlayer_whenPost_thenReturnsCreated() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - PlayerDTO savedPlayer = PlayerDTOFakes.createOneValid(); - savedPlayer.setId(19L); // Simulating auto-generated ID - String body = objectMapper.writeValueAsString(playerDTO); + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + PlayerDTO savedDTO = PlayerDTOFakes.createOneValid(); + savedDTO.setId(19L); // Simulating auto-generated ID + String content = objectMapper.writeValueAsString(dto); Mockito .when(playersServiceMock.create(any(PlayerDTO.class))) - .thenReturn(savedPlayer); + .thenReturn(savedDTO); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .post(PATH) - .content(body) + .content(content) .contentType(MediaType.APPLICATION_JSON); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert + // Then verify(playersServiceMock, times(1)).create(any(PlayerDTO.class)); - assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value()); - assertThat(response.getHeader(HttpHeaders.LOCATION)).isNotNull(); - assertThat(response.getHeader(HttpHeaders.LOCATION)).contains(PATH + "/19"); + then(response.getStatus()).isEqualTo(HttpStatus.CREATED.value()); + then(response.getHeader(HttpHeaders.LOCATION)).isNotNull(); + then(response.getHeader(HttpHeaders.LOCATION)).contains(PATH + "/19"); } /** * Given invalid player data is provided (validation fails) - * When POST /players is called + * When attempting to create a player * Then response status is 400 Bad Request and service is never called */ @Test - void post_invalidPlayer_returnsBadRequest() + void givenInvalidPlayer_whenPost_thenReturnsBadRequest() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneInvalid(); - String body = objectMapper.writeValueAsString(playerDTO); + // Given + PlayerDTO dto = PlayerDTOFakes.createOneInvalid(); + String content = objectMapper.writeValueAsString(dto); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .post(PATH) - .content(body) + .content(content) .contentType(MediaType.APPLICATION_JSON); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert + // Then verify(playersServiceMock, never()).create(any(PlayerDTO.class)); - assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + then(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } /** * Given a player with the same squad number already exists - * When POST /players is called and the service detects a conflict + * When attempting to create a player with a duplicate squad number * Then response status is 409 Conflict */ @Test - void post_squadNumberExists_returnsConflict() + void givenSquadNumberExists_whenPost_thenReturnsConflict() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - String body = objectMapper.writeValueAsString(playerDTO); + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + String content = objectMapper.writeValueAsString(dto); Mockito .when(playersServiceMock.create(any(PlayerDTO.class))) - .thenReturn(null); // Conflict: squad number already exists + .thenReturn(null); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .post(PATH) - .content(body) + .content(content) .contentType(MediaType.APPLICATION_JSON); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert + // Then verify(playersServiceMock, times(1)).create(any(PlayerDTO.class)); - assertThat(response.getStatus()).isEqualTo(HttpStatus.CONFLICT.value()); + then(response.getStatus()).isEqualTo(HttpStatus.CONFLICT.value()); } /* @@ -162,208 +158,209 @@ void post_squadNumberExists_returnsConflict() */ /** - * Given a player with ID 1 exists (Damián Martínez) - * When GET /players/1 is called and the service returns the player - * Then response status is 200 OK and the player data is returned + * Given all players exist in the database + * When requesting all players + * Then response status is 200 OK and all players are returned */ @Test - void getById_playerExists_returnsOkWithPlayer() + void givenPlayersExist_whenGetAll_thenReturnsOkWithAllPlayers() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneForUpdate(); - Long id = 1L; + // Given + List expected = PlayerDTOFakes.createAll(); Mockito - .when(playersServiceMock.retrieveById(id)) - .thenReturn(playerDTO); + .when(playersServiceMock.retrieveAll()) + .thenReturn(expected); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH + "/{id}", id); - // Act + .get(PATH); + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); String content = response.getContentAsString(); - PlayerDTO result = objectMapper.readValue(content, PlayerDTO.class); - // Assert - assertThat(response.getContentType()).contains("application/json"); - verify(playersServiceMock, times(1)).retrieveById(id); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - assertThat(result).usingRecursiveComparison().isEqualTo(playerDTO); + List actual = objectMapper.readValue(content, new TypeReference>() { + }); + // Then + then(response.getContentType()).contains("application/json"); + verify(playersServiceMock, times(1)).retrieveAll(); + then(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + then(actual).usingRecursiveComparison().isEqualTo(expected); } /** - * Given a player with ID 999 does not exist - * When GET /players/999 is called and the service returns null - * Then response status is 404 Not Found + * Given a player exists + * When requesting that player by ID + * Then response status is 200 OK and the player data is returned */ @Test - void getById_playerNotFound_returnsNotFound() + void givenPlayerExists_whenGetById_thenReturnsOk() throws Exception { - // Arrange - Long id = 999L; + // Given + PlayerDTO expected = PlayerDTOFakes.createOneForUpdate(); Mockito - .when(playersServiceMock.retrieveById(anyLong())) - .thenReturn(null); + .when(playersServiceMock.retrieveById(1L)) + .thenReturn(expected); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH + "/{id}", id); - // Act + .get(PATH + "/{id}", 1L); + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert - verify(playersServiceMock, times(1)).retrieveById(anyLong()); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); + String content = response.getContentAsString(); + PlayerDTO actual = objectMapper.readValue(content, PlayerDTO.class); + // Then + then(response.getContentType()).contains("application/json"); + verify(playersServiceMock, times(1)).retrieveById(1L); + then(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + then(actual).usingRecursiveComparison().isEqualTo(expected); } /** - * Given 26 players exist in the database - * When GET /players is called and the service returns all players - * Then response status is 200 OK and all 26 players are returned + * Given a player with a specific ID does not exist + * When requesting that player by ID + * Then response status is 404 Not Found */ @Test - void getAll_playersExist_returnsOkWithAllPlayers() + void givenPlayerDoesNotExist_whenGetById_thenReturnsNotFound() throws Exception { - // Arrange - List playerDTOs = PlayerDTOFakes.createAll(); + // Given + Long id = 999L; Mockito - .when(playersServiceMock.retrieveAll()) - .thenReturn(playerDTOs); + .when(playersServiceMock.retrieveById(anyLong())) + .thenReturn(null); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH); - // Act + .get(PATH + "/{id}", id); + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - String content = response.getContentAsString(); - List result = objectMapper.readValue(content, new TypeReference>() { - }); - // Assert - assertThat(response.getContentType()).contains("application/json"); - verify(playersServiceMock, times(1)).retrieveAll(); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - assertThat(result).hasSize(26); - assertThat(result).usingRecursiveComparison().isEqualTo(playerDTOs); + // Then + verify(playersServiceMock, times(1)).retrieveById(anyLong()); + then(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } /** - * Given 7 players exist in Premier League - * When GET /players/search/league/Premier is called and the service returns matching players - * Then response status is 200 OK and 7 players are returned + * Given a player with a specific squad number exists + * When requesting that player by squad number + * Then response status is 200 OK and the player data is returned */ @Test - void searchByLeague_matchingPlayersExist_returnsOkWithList() + void givenPlayerExists_whenGetBySquadNumber_thenReturnsOk() throws Exception { - // Arrange - List playerDTOs = PlayerDTOFakes.createAll().stream() - .filter(p -> p.getLeague().contains("Premier")) - .toList(); + // Given + Integer squadNumber = 10; + PlayerDTO expected = PlayerDTOFakes.createAll().stream() + .filter(player -> squadNumber.equals(player.getSquadNumber())) + .findFirst() + .orElseThrow(); Mockito - .when(playersServiceMock.searchByLeague(any())) - .thenReturn(playerDTOs); + .when(playersServiceMock.retrieveBySquadNumber(squadNumber)) + .thenReturn(expected); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH + "/search/league/{league}", "Premier"); - // Act + .get(PATH + "/squadnumber/{squadNumber}", squadNumber); + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); String content = response.getContentAsString(); - List result = objectMapper.readValue(content, new TypeReference>() { - }); - // Assert - assertThat(response.getContentType()).contains("application/json"); - verify(playersServiceMock, times(1)).searchByLeague(any()); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - assertThat(result).hasSize(7); - assertThat(result).usingRecursiveComparison().isEqualTo(playerDTOs); + PlayerDTO actual = objectMapper.readValue(content, PlayerDTO.class); + // Then + then(response.getContentType()).contains("application/json"); + verify(playersServiceMock, times(1)).retrieveBySquadNumber(squadNumber); + then(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + then(actual).usingRecursiveComparison().isEqualTo(expected); + then(actual.getSquadNumber()).isEqualTo(squadNumber); } /** - * Given no players exist in "NonexistentLeague" - * When GET /players/search/league/NonexistentLeague is called and the service returns an empty list - * Then response status is 200 OK and an empty list is returned + * Given no player with a specific squad number exists + * When requesting a player by that squad number + * Then response status is 404 Not Found */ @Test - void searchByLeague_noMatches_returnsOkWithEmptyList() + void givenPlayerDoesNotExist_whenGetBySquadNumber_thenReturnsNotFound() throws Exception { - // Arrange + // Given + Integer squadNumber = 99; Mockito - .when(playersServiceMock.searchByLeague(any())) - .thenReturn(List.of()); + .when(playersServiceMock.retrieveBySquadNumber(squadNumber)) + .thenReturn(null); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH + "/search/league/{league}", "NonexistentLeague"); - // Act + .get(PATH + "/squadnumber/{squadNumber}", squadNumber); + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - String content = response.getContentAsString(); - List result = objectMapper.readValue(content, new TypeReference>() { - }); - // Assert - assertThat(response.getContentType()).contains("application/json"); - verify(playersServiceMock, times(1)).searchByLeague(any()); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - assertThat(result).isEmpty(); + // Then + verify(playersServiceMock, times(1)).retrieveBySquadNumber(squadNumber); + then(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } /** - * Given a player with squad number 10 exists (Messi) - * When GET /players/search/squadnumber/10 is called and the service returns the player - * Then response status is 200 OK and the player data is returned + * Given players exist in a specific league + * When searching for players by league name + * Then response status is 200 OK and matching players are returned */ @Test - void searchBySquadNumber_playerExists_returnsOkWithPlayer() + void givenPlayersExist_whenSearchByLeague_thenReturnsOk() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createAll().stream() - .filter(player -> player.getSquadNumber() == 10) - .findFirst() - .orElseThrow(); + // Given + String league = "Premier"; + List expected = PlayerDTOFakes.createAll().stream() + .filter(p -> p.getLeague().contains(league)) + .toList(); Mockito - .when(playersServiceMock.searchBySquadNumber(10)) - .thenReturn(playerDTO); + .when(playersServiceMock.searchByLeague(any())) + .thenReturn(expected); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH + "/search/squadnumber/{squadNumber}", 10); - // Act + .get(PATH + "/search/league/{league}", league); + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); String content = response.getContentAsString(); - PlayerDTO result = objectMapper.readValue(content, PlayerDTO.class); - // Assert - assertThat(response.getContentType()).contains("application/json"); - verify(playersServiceMock, times(1)).searchBySquadNumber(10); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - assertThat(result).usingRecursiveComparison().isEqualTo(playerDTO); - assertThat(result.getSquadNumber()).isEqualTo(10); + List actual = objectMapper.readValue(content, new TypeReference>() { + }); + // Then + then(response.getContentType()).contains("application/json"); + verify(playersServiceMock, times(1)).searchByLeague(any()); + then(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + then(actual).usingRecursiveComparison().isEqualTo(expected); } /** - * Given no player with squad number 99 exists - * When GET /players/search/squadnumber/99 is called and the service returns null - * Then response status is 404 Not Found + * Given no players exist in a specific league + * When searching for players by that league name + * Then response status is 200 OK and an empty list is returned */ @Test - void searchBySquadNumber_playerNotFound_returnsNotFound() + void givenNoPlayersExist_whenSearchByLeague_thenReturnsOk() throws Exception { - // Arrange + // Given + String league = "Nonexistent League"; Mockito - .when(playersServiceMock.searchBySquadNumber(99)) - .thenReturn(null); + .when(playersServiceMock.searchByLeague(any())) + .thenReturn(List.of()); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH + "/search/squadnumber/{squadNumber}", 99); - // Act + .get(PATH + "/search/league/{league}", league); + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert - verify(playersServiceMock, times(1)).searchBySquadNumber(99); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); + String content = response.getContentAsString(); + List actual = objectMapper.readValue(content, new TypeReference>() { + }); + // Then + then(response.getContentType()).contains("application/json"); + verify(playersServiceMock, times(1)).searchByLeague(any()); + then(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + then(actual).isEmpty(); } /* @@ -374,145 +371,142 @@ void searchBySquadNumber_playerNotFound_returnsNotFound() /** * Given a player exists and valid update data is provided - * When PUT /players/{id} is called and the service successfully updates the player + * When updating that player * Then response status is 204 No Content */ @Test - void put_playerExists_returnsNoContent() + void givenPlayerExists_whenPut_thenReturnsNoContent() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - playerDTO.setId(1L); // Set ID for update operation - Long id = playerDTO.getId(); - String body = objectMapper.writeValueAsString(playerDTO); + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + dto.setId(1L); // Set ID for update operation + String content = objectMapper.writeValueAsString(dto); Mockito .when(playersServiceMock.update(any(PlayerDTO.class))) .thenReturn(true); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put(PATH + "/{id}", id) - .content(body) + .put(PATH + "/{id}", dto.getId()) + .content(content) .contentType(MediaType.APPLICATION_JSON); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert + // Then verify(playersServiceMock, times(1)).update(any(PlayerDTO.class)); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); + then(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); } /** * Given a player with the provided ID does not exist - * When PUT /players/{id} is called and the service returns false + * When attempting to update that player * Then response status is 404 Not Found */ @Test - void put_playerNotFound_returnsNotFound() + void givenPlayerDoesNotExist_whenPut_thenReturnsNotFound() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - playerDTO.setId(999L); // Set ID for update operation - Long id = playerDTO.getId(); - String body = objectMapper.writeValueAsString(playerDTO); + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + dto.setId(999L); // Set ID for update operation + String content = objectMapper.writeValueAsString(dto); Mockito .when(playersServiceMock.update(any(PlayerDTO.class))) .thenReturn(false); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put(PATH + "/{id}", id) - .content(body) + .put(PATH + "/{id}", dto.getId()) + .content(content) .contentType(MediaType.APPLICATION_JSON); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert + // Then verify(playersServiceMock, times(1)).update(any(PlayerDTO.class)); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); + then(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } /** * Given invalid player data is provided (validation fails) - * When PUT /players/{id} is called + * When attempting to update a player * Then response status is 400 Bad Request and service is never called */ @Test - void put_invalidPlayer_returnsBadRequest() + void givenInvalidPlayer_whenPut_thenReturnsBadRequest() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneInvalid(); - Long id = playerDTO.getId(); - String body = objectMapper.writeValueAsString(playerDTO); + // Given + PlayerDTO dto = PlayerDTOFakes.createOneInvalid(); + String content = objectMapper.writeValueAsString(dto); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put(PATH + "/{id}", id) - .content(body) + .put(PATH + "/{id}", dto.getId()) + .content(content) .contentType(MediaType.APPLICATION_JSON); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert + // Then verify(playersServiceMock, never()).update(any(PlayerDTO.class)); - assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + then(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } /** * Given the path ID does not match the body ID - * When PUT /players/{id} is called + * When attempting to update a player * Then response status is 400 Bad Request and service is never called */ @Test - void put_idMismatch_returnsBadRequest() + void givenIdMismatch_whenPut_thenReturnsBadRequest() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - playerDTO.setId(999L); // Body has different ID + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + dto.setId(999L); // Body has different ID Long pathId = 1L; // Path has different ID - String body = objectMapper.writeValueAsString(playerDTO); + String content = objectMapper.writeValueAsString(dto); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .put(PATH + "/{id}", pathId) - .content(body) + .content(content) .contentType(MediaType.APPLICATION_JSON); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert + // Then verify(playersServiceMock, never()).update(any(PlayerDTO.class)); - assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + then(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } /** * Given the body ID is null (ID only in path) - * When PUT /players/{id} is called + * When updating a player * Then the ID is set from the path and the update proceeds normally */ @Test - void put_nullBodyId_setsIdFromPath() + void givenNullBodyId_whenPut_thenSetsIdFromPath() throws Exception { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - playerDTO.setId(null); // Body has null ID + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + dto.setId(null); // Body has null ID Long pathId = 1L; - String body = objectMapper.writeValueAsString(playerDTO); + String content = objectMapper.writeValueAsString(dto); Mockito .when(playersServiceMock.update(any(PlayerDTO.class))) .thenReturn(true); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .put(PATH + "/{id}", pathId) - .content(body) + .content(content) .contentType(MediaType.APPLICATION_JSON); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert + // Then verify(playersServiceMock, times(1)).update(any(PlayerDTO.class)); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); + then(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); } /* @@ -522,52 +516,51 @@ void put_nullBodyId_setsIdFromPath() */ /** - * Given a player with ID 1 exists - * When DELETE /players/1 is called and the service successfully deletes the player + * Given a player exists + * When deleting that player by ID * Then response status is 204 No Content */ @Test - void delete_playerExists_returnsNoContent() + void givenPlayerExists_whenDelete_thenReturnsNoContent() throws Exception { - // Arrange - Long id = 1L; + // Given Mockito - .when(playersServiceMock.delete(anyLong())) + .when(playersServiceMock.delete(1L)) .thenReturn(true); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete(PATH + "/{id}", id); - // Act + .delete(PATH + "/{id}", 1L); + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert - verify(playersServiceMock, times(1)).delete(anyLong()); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); + // Then + verify(playersServiceMock, times(1)).delete(1L); + then(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); } /** - * Given a player with ID 999 does not exist - * When DELETE /players/999 is called and the service returns false + * Given a player with a specific ID does not exist + * When attempting to delete that player by ID * Then response status is 404 Not Found */ @Test - void delete_playerNotFound_returnsNotFound() + void givenPlayerDoesNotExist_whenDelete_thenReturnsNotFound() throws Exception { - // Arrange + // Given Long id = 999L; Mockito - .when(playersServiceMock.delete(anyLong())) + .when(playersServiceMock.delete(id)) .thenReturn(false); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .delete(PATH + "/{id}", id); - // Act + // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); - // Assert - verify(playersServiceMock, times(1)).delete(anyLong()); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); + // Then + verify(playersServiceMock, times(1)).delete(id); + then(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } } diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/PlayersRepositoryTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/PlayersRepositoryTests.java index 529a6cb..8ee6b04 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/PlayersRepositoryTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/PlayersRepositoryTests.java @@ -1,6 +1,6 @@ package ar.com.nanotaboada.java.samples.spring.boot.test.repositories; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.BDDAssertions.then; import java.util.List; import java.util.Optional; @@ -27,88 +27,97 @@ class PlayersRepositoryTests { private PlayersRepository repository; /** - * Given Leandro Paredes is created and saved to the database with an auto-generated ID - * When findById() is called with that ID + * Given a player exists in the database + * When findById() is called with that player's ID * Then the player is returned */ @Test - void findById_playerExists_returnsPlayer() { - // Arrange - Player leandro = PlayerFakes.createOneValid(); - Player saved = repository.save(leandro); - // Act - Optional actual = repository.findById(saved.getId()); - // Assert - assertThat(actual).isPresent(); - assertThat(actual.get()).usingRecursiveComparison().isEqualTo(saved); + void givenPlayerExists_whenFindById_thenReturnsPlayer() { + // Given + Player expected = repository.save(PlayerFakes.createOneValid()); + // When + Optional actual = repository.findById(expected.getId()); + // Then + then(actual).isPresent(); + then(actual.get()).usingRecursiveComparison().isEqualTo(expected); } /** - * Given the database does not contain a player with ID 999 - * When findById(999) is called + * Given the database does not contain a player with a specific ID + * When querying by that ID * Then an empty Optional is returned */ @Test - void findById_playerNotFound_returnsEmpty() { - // Act - Optional actual = repository.findById(999L); - // Assert - assertThat(actual).isEmpty(); + void givenPlayerDoesNotExist_whenFindById_thenReturnsEmpty() { + // Given + Long nonExistentId = 999L; + // When + Optional actual = repository.findById(nonExistentId); + // Then + then(actual).isEmpty(); } /** - * Given the database contains 7 players in Premier League (pre-seeded in-memory database) - * When findByLeagueContainingIgnoreCase("Premier") is called + * Given players exist in a specific league (pre-seeded from dml.sql) + * When searching by league name * Then a list of matching players is returned */ @Test - void findByLeague_matchingPlayersExist_returnsList() { - // Act - List actual = repository.findByLeagueContainingIgnoreCase("Premier"); - // Assert - assertThat(actual).isNotEmpty() - .allMatch(player -> player.getLeague().toLowerCase().contains("premier")); + void givenPlayersExist_whenFindByLeague_thenReturnsList() { + // Given + String leagueName = "Premier"; + // When + List actual = repository.findByLeagueContainingIgnoreCase(leagueName); + // Then + then(actual).isNotEmpty() + .allMatch(player -> player.getLeague().toLowerCase().contains(leagueName.toLowerCase())); } /** - * Given the database does not contain players in "nonexistentleague" - * When findByLeagueContainingIgnoreCase("nonexistentleague") is called + * Given the database does not contain players in a specific league + * When searching by that league name * Then an empty list is returned */ @Test - void findByLeague_noMatches_returnsEmptyList() { - // Act - List actual = repository.findByLeagueContainingIgnoreCase("nonexistentleague"); - // Assert - assertThat(actual).isEmpty(); + void givenNoPlayersExist_whenFindByLeague_thenReturnsEmptyList() { + // Given + String nonExistentLeague = "Nonexistent League"; + // When + List actual = repository.findByLeagueContainingIgnoreCase(nonExistentLeague); + // Then + then(actual).isEmpty(); } /** - * Given the database contains Messi with squad number 10 (pre-seeded at ID 10) - * When findBySquadNumber(10) is called - * Then Messi's player record is returned + * Given a player with a specific squad number exists (pre-seeded from dml.sql) + * When querying by that squad number + * Then the player record is returned */ @Test - void findBySquadNumber_playerExists_returnsPlayer() { - // Act - Optional actual = repository.findBySquadNumber(10); - // Assert - assertThat(actual).isPresent(); - assertThat(actual.get()) - .extracting(Player::getSquadNumber, Player::getLastName) - .containsExactly(10, "Messi"); + void givenPlayerExists_whenFindBySquadNumber_thenReturnsPlayer() { + // Given + Integer messiSquadNumber = 10; // Pre-seeded from dml.sql + String expectedLastName = "Messi"; + // When + Optional actual = repository.findBySquadNumber(messiSquadNumber); + // Then + then(actual).isPresent(); + then(actual.get().getSquadNumber()).isEqualTo(messiSquadNumber); + then(actual.get().getLastName()).isEqualTo(expectedLastName); } /** - * Given the database does not contain a player with squad number 99 - * When findBySquadNumber(99) is called + * Given the database does not contain a player with a specific squad number + * When querying by that squad number * Then an empty Optional is returned */ @Test - void findBySquadNumber_playerNotFound_returnsEmpty() { - // Act - Optional actual = repository.findBySquadNumber(99); - // Assert - assertThat(actual).isEmpty(); + void givenPlayerDoesNotExist_whenFindBySquadNumber_thenReturnsEmpty() { + // Given + Integer nonExistentSquadNumber = 99; + // When + Optional actual = repository.findBySquadNumber(nonExistentSquadNumber); + // Then + then(actual).isEmpty(); } } diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java index 9a40917..4ae52d9 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java @@ -1,6 +1,6 @@ package ar.com.nanotaboada.java.samples.spring.boot.test.services; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.BDDAssertions.then; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.never; @@ -47,59 +47,60 @@ class PlayersServiceTests { */ /** - * Given the repository does not contain a player with the same squad number + * Given no existing player with the same squad number * When create() is called with valid player data * Then the player is saved and a PlayerDTO is returned */ @Test - void create_noConflict_returnsPlayerDTO() { - // Arrange - Player player = PlayerFakes.createOneValid(); - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); + void givenNoExistingPlayer_whenCreate_thenReturnsPlayerDTO() { + // Given + Player entity = PlayerFakes.createOneValid(); + PlayerDTO expected = PlayerDTOFakes.createOneValid(); Mockito - .when(playersRepositoryMock.findBySquadNumber(playerDTO.getSquadNumber())) + .when(playersRepositoryMock.findBySquadNumber(expected.getSquadNumber())) .thenReturn(Optional.empty()); // No conflict Mockito - .when(modelMapperMock.map(playerDTO, Player.class)) - .thenReturn(player); + .when(modelMapperMock.map(expected, Player.class)) + .thenReturn(entity); Mockito .when(playersRepositoryMock.save(any(Player.class))) - .thenReturn(player); + .thenReturn(entity); Mockito - .when(modelMapperMock.map(player, PlayerDTO.class)) - .thenReturn(playerDTO); - // Act - PlayerDTO result = playersService.create(playerDTO); - // Assert - verify(playersRepositoryMock, times(1)).findBySquadNumber(playerDTO.getSquadNumber()); + .when(modelMapperMock.map(entity, PlayerDTO.class)) + .thenReturn(expected); + // When + PlayerDTO actual = playersService.create(expected); + // Then + verify(playersRepositoryMock, times(1)).findBySquadNumber(expected.getSquadNumber()); verify(playersRepositoryMock, times(1)).save(any(Player.class)); - verify(modelMapperMock, times(1)).map(playerDTO, Player.class); - verify(modelMapperMock, times(1)).map(player, PlayerDTO.class); - assertThat(result).isEqualTo(playerDTO); + verify(modelMapperMock, times(1)).map(expected, Player.class); + verify(modelMapperMock, times(1)).map(entity, PlayerDTO.class); + then(actual).isEqualTo(expected); } /** - * Given the repository finds an existing player with the same squad number + * Given a player with the same squad number already exists * When create() is called * Then null is returned (conflict detected) */ @Test - void create_squadNumberExists_returnsNull() { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); + void givenPlayerAlreadyExists_whenCreate_thenReturnsNull() { + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + Integer expectedSquadNumber = 10; Player existingPlayer = PlayerFakes.createAll().stream() - .filter(player -> player.getSquadNumber() == 10) + .filter(player -> expectedSquadNumber.equals(player.getSquadNumber())) .findFirst() .orElseThrow(); Mockito - .when(playersRepositoryMock.findBySquadNumber(playerDTO.getSquadNumber())) + .when(playersRepositoryMock.findBySquadNumber(dto.getSquadNumber())) .thenReturn(Optional.of(existingPlayer)); - // Act - PlayerDTO result = playersService.create(playerDTO); - // Assert - verify(playersRepositoryMock, times(1)).findBySquadNumber(playerDTO.getSquadNumber()); + // When + PlayerDTO actual = playersService.create(dto); + // Then + verify(playersRepositoryMock, times(1)).findBySquadNumber(dto.getSquadNumber()); verify(playersRepositoryMock, never()).save(any(Player.class)); - assertThat(result).isNull(); + then(actual).isNull(); } /** @@ -108,25 +109,25 @@ void create_squadNumberExists_returnsNull() { * Then null is returned (conflict detected via exception) */ @Test - void create_raceConditionOnSave_returnsNull() { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - Player player = PlayerFakes.createOneValid(); + void givenRaceCondition_whenCreate_thenReturnsNull() { + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + Player entity = PlayerFakes.createOneValid(); Mockito - .when(playersRepositoryMock.findBySquadNumber(playerDTO.getSquadNumber())) + .when(playersRepositoryMock.findBySquadNumber(dto.getSquadNumber())) .thenReturn(Optional.empty()); // No conflict initially Mockito - .when(modelMapperMock.map(playerDTO, Player.class)) - .thenReturn(player); + .when(modelMapperMock.map(dto, Player.class)) + .thenReturn(entity); Mockito .when(playersRepositoryMock.save(any(Player.class))) .thenThrow(new DataIntegrityViolationException("Unique constraint violation")); - // Act - PlayerDTO result = playersService.create(playerDTO); - // Assert - verify(playersRepositoryMock, times(1)).findBySquadNumber(playerDTO.getSquadNumber()); + // When + PlayerDTO actual = playersService.create(dto); + // Then + verify(playersRepositoryMock, times(1)).findBySquadNumber(dto.getSquadNumber()); verify(playersRepositoryMock, times(1)).save(any(Player.class)); - assertThat(result).isNull(); + then(actual).isNull(); } /* @@ -136,72 +137,125 @@ void create_raceConditionOnSave_returnsNull() { */ /** - * Given the repository finds an existing player with ID 1 (Damián Martínez) - * When retrieveById(1) is called + * Given all players exist in the repository + * When retrieveAll() is called + * Then a list of all player DTOs is returned + */ + @Test + void givenAllPlayersExist_whenRetrieveAll_thenReturns26Players() { + // Given + List entities = PlayerFakes.createAll(); + List dtos = PlayerDTOFakes.createAll(); + Mockito + .when(playersRepositoryMock.findAll()) + .thenReturn(entities); + // Mock modelMapper to convert each player correctly + for (int i = 0; i < entities.size(); i++) { + Mockito + .when(modelMapperMock.map(entities.get(i), PlayerDTO.class)) + .thenReturn(dtos.get(i)); + } + // When + List actual = playersService.retrieveAll(); + // Then + verify(playersRepositoryMock, times(1)).findAll(); + then(actual).usingRecursiveComparison().isEqualTo(dtos); + } + + /** + * Given a player exists with a specific ID + * When retrieving that player by ID * Then the corresponding PlayerDTO is returned */ @Test - void retrieveById_playerExists_returnsPlayerDTO() { - // Arrange - Player player = PlayerFakes.createOneForUpdate(); - PlayerDTO playerDTO = PlayerDTOFakes.createOneForUpdate(); + void givenPlayerExists_whenRetrieveById_thenReturnsPlayerDTO() { + // Given + Player entity = PlayerFakes.createOneForUpdate(); + PlayerDTO expected = PlayerDTOFakes.createOneForUpdate(); Mockito .when(playersRepositoryMock.findById(1L)) - .thenReturn(Optional.of(player)); + .thenReturn(Optional.of(entity)); Mockito - .when(modelMapperMock.map(player, PlayerDTO.class)) - .thenReturn(playerDTO); - // Act - PlayerDTO result = playersService.retrieveById(1L); - // Assert + .when(modelMapperMock.map(entity, PlayerDTO.class)) + .thenReturn(expected); + // When + PlayerDTO actual = playersService.retrieveById(1L); + // Then verify(playersRepositoryMock, times(1)).findById(1L); - verify(modelMapperMock, times(1)).map(player, PlayerDTO.class); - assertThat(result).isEqualTo(playerDTO); + verify(modelMapperMock, times(1)).map(entity, PlayerDTO.class); + then(actual).isEqualTo(expected); } /** - * Given the repository does not find a player with ID 999 - * When retrieveById(999) is called + * Given no player exists with a specific ID + * When retrieving by that ID * Then null is returned */ @Test - void retrieveById_playerNotFound_returnsNull() { - // Arrange + void givenPlayerDoesNotExist_whenRetrieveById_thenReturnsNull() { + // Given + Long id = 999L; Mockito .when(playersRepositoryMock.findById(anyLong())) .thenReturn(Optional.empty()); - // Act - PlayerDTO result = playersService.retrieveById(999L); - // Assert + // When + PlayerDTO actual = playersService.retrieveById(id); + // Then verify(playersRepositoryMock, times(1)).findById(anyLong()); verify(modelMapperMock, never()).map(any(Player.class), any()); - assertThat(result).isNull(); + then(actual).isNull(); } /** - * Given the repository returns all 26 players - * When retrieveAll() is called - * Then a list of 26 PlayerDTOs is returned + * Given a player with a specific squad number exists + * When retrieving by that squad number + * Then the corresponding PlayerDTO is returned */ @Test - void retrieveAll_playersExist_returnsListOfPlayerDTOs() { - // Arrange - List players = PlayerFakes.createAll(); - List playerDTOs = PlayerDTOFakes.createAll(); + void givenPlayerExists_whenRetrieveBySquadNumber_thenReturnsPlayerDTO() { + // Given + Integer squadNumber = 10; + Player entity = PlayerFakes.createAll().stream() + .filter(player -> squadNumber.equals(player.getSquadNumber())) + .findFirst() + .orElseThrow(); + PlayerDTO expected = PlayerDTOFakes.createAll().stream() + .filter(player -> squadNumber.equals(player.getSquadNumber())) + .findFirst() + .orElseThrow(); Mockito - .when(playersRepositoryMock.findAll()) - .thenReturn(players); - // Mock modelMapper to convert each player correctly - for (int i = 0; i < players.size(); i++) { - Mockito - .when(modelMapperMock.map(players.get(i), PlayerDTO.class)) - .thenReturn(playerDTOs.get(i)); - } - // Act - List result = playersService.retrieveAll(); - // Assert - verify(playersRepositoryMock, times(1)).findAll(); - assertThat(result).hasSize(26); + .when(playersRepositoryMock.findBySquadNumber(squadNumber)) + .thenReturn(Optional.of(entity)); + Mockito + .when(modelMapperMock.map(entity, PlayerDTO.class)) + .thenReturn(expected); + // When + PlayerDTO actual = playersService.retrieveBySquadNumber(squadNumber); + // Then + verify(playersRepositoryMock, times(1)).findBySquadNumber(squadNumber); + verify(modelMapperMock, times(1)).map(entity, PlayerDTO.class); + then(actual).isEqualTo(expected); + then(actual.getSquadNumber()).isEqualTo(squadNumber); + } + + /** + * Given no player exists with a specific squad number + * When retrieving by that squad number + * Then null is returned + */ + @Test + void givenPlayerDoesNotExist_whenRetrieveBySquadNumber_thenReturnsNull() { + // Given + Integer squadNumber = 99; + Mockito + .when(playersRepositoryMock.findBySquadNumber(squadNumber)) + .thenReturn(Optional.empty()); + // When + PlayerDTO actual = playersService.retrieveBySquadNumber(squadNumber); + // Then + verify(playersRepositoryMock, times(1)).findBySquadNumber(squadNumber); + verify(modelMapperMock, never()).map(any(Player.class), any()); + then(actual).isNull(); } /* @@ -211,102 +265,57 @@ void retrieveAll_playersExist_returnsListOfPlayerDTOs() { */ /** - * Given the repository returns 7 players matching "Premier" league - * When searchByLeague("Premier") is called - * Then a list of 7 PlayerDTOs is returned + * Given players exist in a specific league + * When searching by league name + * Then a list of matching player DTOs is returned */ @Test - void searchByLeague_matchingPlayersExist_returnsListOfPlayerDTOs() { - // Arrange - List players = PlayerFakes.createAll().stream() - .filter(p -> p.getLeague().contains("Premier")) + void givenPlayersExist_whenSearchByLeague_thenReturns7Players() { + // Given + String league = "Premier"; + List entities = PlayerFakes.createAll().stream() + .filter(player -> player.getLeague().contains(league)) .toList(); - List playerDTOs = PlayerDTOFakes.createAll().stream() - .filter(p -> p.getLeague().contains("Premier")) + List expected = PlayerDTOFakes.createAll().stream() + .filter(player -> player.getLeague().contains(league)) .toList(); Mockito .when(playersRepositoryMock.findByLeagueContainingIgnoreCase(any())) - .thenReturn(players); + .thenReturn(entities); // Mock modelMapper to convert each player correctly - for (int i = 0; i < players.size(); i++) { + for (int i = 0; i < entities.size(); i++) { Mockito - .when(modelMapperMock.map(players.get(i), PlayerDTO.class)) - .thenReturn(playerDTOs.get(i)); + .when(modelMapperMock.map(entities.get(i), PlayerDTO.class)) + .thenReturn(expected.get(i)); } - // Act - List result = playersService.searchByLeague("Premier"); - // Assert + // When + List actual = playersService.searchByLeague(league); + // Then verify(playersRepositoryMock, times(1)).findByLeagueContainingIgnoreCase(any()); - assertThat(result).hasSize(7); + then(actual) + .hasSize(expected.size()) + .usingRecursiveComparison() + .isEqualTo(expected); } /** - * Given the repository returns an empty list for "NonexistentLeague" - * When searchByLeague("NonexistentLeague") is called + * Given no players exist in a specific league + * When searching by that league name * Then an empty list is returned */ @Test - void searchByLeague_noMatches_returnsEmptyList() { - // Arrange + void givenNoPlayersExist_whenSearchByLeague_thenReturnsEmptyList() { + // Given + String league = "Nonexistent League"; Mockito .when(playersRepositoryMock.findByLeagueContainingIgnoreCase(any())) .thenReturn(List.of()); - // Act - List result = playersService.searchByLeague("NonexistentLeague"); - // Assert + // When + List actual = playersService.searchByLeague(league); + // Then verify(playersRepositoryMock, times(1)).findByLeagueContainingIgnoreCase(any()); verify(modelMapperMock, never()).map(any(Player.class), any()); - assertThat(result).isEmpty(); - } - - /** - * Given the repository finds Messi with squad number 10 - * When searchBySquadNumber(10) is called - * Then the corresponding PlayerDTO is returned - */ - @Test - void searchBySquadNumber_playerExists_returnsPlayerDTO() { - // Arrange - Player player = PlayerFakes.createAll().stream() - .filter(p -> p.getSquadNumber() == 10) - .findFirst() - .orElseThrow(); - PlayerDTO playerDTO = PlayerDTOFakes.createAll().stream() - .filter(p -> p.getSquadNumber() == 10) - .findFirst() - .orElseThrow(); - Mockito - .when(playersRepositoryMock.findBySquadNumber(10)) - .thenReturn(Optional.of(player)); - Mockito - .when(modelMapperMock.map(player, PlayerDTO.class)) - .thenReturn(playerDTO); - // Act - PlayerDTO result = playersService.searchBySquadNumber(10); - // Assert - verify(playersRepositoryMock, times(1)).findBySquadNumber(10); - verify(modelMapperMock, times(1)).map(player, PlayerDTO.class); - assertThat(result).isEqualTo(playerDTO); - assertThat(result.getSquadNumber()).isEqualTo(10); - } - - /** - * Given the repository does not find a player with squad number 99 - * When searchBySquadNumber(99) is called - * Then null is returned - */ - @Test - void searchBySquadNumber_playerNotFound_returnsNull() { - // Arrange - Mockito - .when(playersRepositoryMock.findBySquadNumber(99)) - .thenReturn(Optional.empty()); - // Act - PlayerDTO result = playersService.searchBySquadNumber(99); - // Assert - verify(playersRepositoryMock, times(1)).findBySquadNumber(99); - verify(modelMapperMock, never()).map(any(Player.class), any()); - assertThat(result).isNull(); + then(actual).isEmpty(); } /* @@ -316,50 +325,50 @@ void searchBySquadNumber_playerNotFound_returnsNull() { */ /** - * Given the repository confirms player with ID 1 exists - * When update() is called with updated data (Damián→Emiliano Martínez) - * Then the player is saved and true is returned + * Given a player exists + * When update() is called with modified player data + * Then the player is updated and true is returned */ @Test - void update_playerExists_savesAndReturnsTrue() { - // Arrange - Player playerUpdated = PlayerFakes.createOneUpdated(); - PlayerDTO playerDTO = PlayerDTOFakes.createOneUpdated(); + void givenPlayerExists_whenUpdate_thenReturnsTrue() { + // Given + Player entity = PlayerFakes.createOneUpdated(); + PlayerDTO dto = PlayerDTOFakes.createOneUpdated(); Mockito .when(playersRepositoryMock.existsById(1L)) .thenReturn(true); Mockito - .when(modelMapperMock.map(playerDTO, Player.class)) - .thenReturn(playerUpdated); - // Act - boolean result = playersService.update(playerDTO); - // Assert + .when(modelMapperMock.map(dto, Player.class)) + .thenReturn(entity); + // When + boolean actual = playersService.update(dto); + // Then verify(playersRepositoryMock, times(1)).existsById(1L); verify(playersRepositoryMock, times(1)).save(any(Player.class)); - verify(modelMapperMock, times(1)).map(playerDTO, Player.class); - assertThat(result).isTrue(); + verify(modelMapperMock, times(1)).map(dto, Player.class); + then(actual).isTrue(); } /** - * Given the repository confirms player with ID 999 does not exist + * Given no player exists with the specified ID * When update() is called * Then false is returned without saving */ @Test - void update_playerNotFound_returnsFalse() { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - playerDTO.setId(999L); + void givenPlayerDoesNotExist_whenUpdate_thenReturnsFalse() { + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + dto.setId(999L); Mockito .when(playersRepositoryMock.existsById(999L)) .thenReturn(false); - // Act - boolean result = playersService.update(playerDTO); - // Assert + // When + boolean actual = playersService.update(dto); + // Then verify(playersRepositoryMock, times(1)).existsById(999L); verify(playersRepositoryMock, never()).save(any(Player.class)); - verify(modelMapperMock, never()).map(playerDTO, Player.class); - assertThat(result).isFalse(); + verify(modelMapperMock, never()).map(dto, Player.class); + then(actual).isFalse(); } /** @@ -368,17 +377,17 @@ void update_playerNotFound_returnsFalse() { * Then false is returned without checking repository or saving */ @Test - void update_nullId_returnsFalse() { - // Arrange - PlayerDTO playerDTO = PlayerDTOFakes.createOneValid(); - playerDTO.setId(null); - // Act - boolean result = playersService.update(playerDTO); - // Assert + void givenNullId_whenUpdate_thenReturnsFalse() { + // Given + PlayerDTO dto = PlayerDTOFakes.createOneValid(); + dto.setId(null); + // When + boolean actual = playersService.update(dto); + // Then verify(playersRepositoryMock, never()).existsById(any()); verify(playersRepositoryMock, never()).save(any(Player.class)); verify(modelMapperMock, never()).map(any(), any()); - assertThat(result).isFalse(); + then(actual).isFalse(); } /* @@ -388,40 +397,40 @@ void update_nullId_returnsFalse() { */ /** - * Given the repository confirms player with ID 21 exists (Alejandro Gómez) - * When delete(21) is called + * Given a player exists + * When deleting that player * Then the player is deleted and true is returned */ @Test - void delete_playerExists_deletesAndReturnsTrue() { - // Arrange + void givenPlayerExists_whenDelete_thenReturnsTrue() { + // Given Mockito .when(playersRepositoryMock.existsById(21L)) .thenReturn(true); - // Act - boolean result = playersService.delete(21L); - // Assert + // When + boolean actual = playersService.delete(21L); + // Then verify(playersRepositoryMock, times(1)).existsById(21L); verify(playersRepositoryMock, times(1)).deleteById(21L); - assertThat(result).isTrue(); + then(actual).isTrue(); } /** - * Given the repository confirms player with ID 999 does not exist - * When delete(999) is called + * Given no player exists with a specific ID + * When attempting to delete that player * Then false is returned without deleting */ @Test - void delete_playerNotFound_returnsFalse() { - // Arrange + void givenPlayerDoesNotExist_whenDelete_thenReturnsFalse() { + // Given Mockito .when(playersRepositoryMock.existsById(999L)) .thenReturn(false); - // Act - boolean result = playersService.delete(999L); - // Assert + // When + boolean actual = playersService.delete(999L); + // Then verify(playersRepositoryMock, times(1)).existsById(999L); verify(playersRepositoryMock, never()).deleteById(anyLong()); - assertThat(result).isFalse(); + then(actual).isFalse(); } }