Skip to content

Commit 1266928

Browse files
authored
Merge pull request #266 from nanotaboada/test/bdd-given-when-then-pattern
test: adopt BDD Given-When-Then pattern across all tests
2 parents 8cd753b + a0bd0b1 commit 1266928

File tree

9 files changed

+590
-581
lines changed

9 files changed

+590
-581
lines changed

.coderabbit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ reviews:
8484
- Test classes should use JUnit 5 (Jupiter)
8585
- Verify proper Spring test annotations (@WebMvcTest, @DataJpaTest, etc.)
8686
- Check use of @MockitoBean (Spring Boot 4.0 style)
87-
- Ensure test naming follows method_scenario_outcome pattern (concise)
87+
- Ensure test naming follows givenX_whenY_thenZ BDD pattern
8888
- Validate BDD semantics in JavaDoc comments (Given/When/Then)
8989
- Validate use of AssertJ for fluent assertions
9090
- Check @DisplayName for readable test descriptions

.github/copilot-instructions.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ http://localhost:9001/actuator/health # Health check
4343
- **Validation**: Bean Validation annotations in DTOs (`@NotNull`, `@Min`, etc.)
4444
- **Caching**: Spring `@Cacheable` on service layer (1-hour TTL)
4545
- **DTO Pattern**: ModelMapper for entity ↔ DTO transformations
46-
- **Repository**: Spring Data JPA with derived queries and custom JPQL
46+
- **Repository**: Spring Data JPA with derived queries (prefer over custom JPQL)
4747

4848
## Code Conventions
4949

@@ -67,21 +67,21 @@ http://localhost:9001/actuator/health # Health check
6767
## Testing
6868

6969
- **Structure**: `*Tests.java` in `src/test/java/` (mirrors main package structure)
70-
- **Naming Pattern**: `method_scenario_outcome`
71-
- `method`: Method under test (e.g., `post`, `findById`, `create`)
72-
- `scenario`: Context (e.g., `playerExists`, `invalidData`, `noMatches`)
73-
- `outcome`: Expected result (e.g., `returnsPlayer`, `returnsConflict`, `returnsEmpty`)
70+
- **Naming Pattern**: `givenX_whenY_thenZ` (BDD Given-When-Then)
71+
- `given`: Preconditions/context (e.g., `givenPlayerExists`, `givenInvalidData`, `givenNoMatches`)
72+
- `when`: Action being tested (e.g., `whenPost`, `whenFindById`, `whenCreate`)
73+
- `then`: Expected outcome (e.g., `thenReturnsPlayer`, `thenReturnsConflict`, `thenReturnsEmpty`)
7474
- **Examples**:
7575

7676
```java
7777
// Controller
78-
void post_squadNumberExists_returnsConflict()
78+
void givenSquadNumberExists_whenPost_thenReturnsConflict()
7979

8080
// Service
81-
void create_noConflict_returnsPlayerDTO()
81+
void givenNoConflict_whenCreate_thenReturnsPlayerDTO()
8282

8383
// Repository
84-
void findById_playerExists_returnsPlayer()
84+
void givenPlayerExists_whenFindById_thenReturnsPlayer()
8585
```
8686

8787
- **JavaDoc**: BDD Given/When/Then structure in test comments
@@ -97,7 +97,7 @@ http://localhost:9001/actuator/health # Health check
9797
```
9898

9999
- **Annotations**: `@SpringBootTest`, `@AutoConfigureMockMvc`, `@Test`
100-
- **Assertions**: AssertJ (fluent, e.g., `assertThat(result).isNotNull()`)
100+
- **Assertions**: AssertJ BDD style (e.g., `then(result).isNotNull()`)
101101
- **Mocking**: Mockito for service layer tests
102102
- **Database**: Tests use in-memory SQLite (auto-cleared after each test)
103103
- **Coverage**: Target high coverage (JaCoCo reports in `target/site/jacoco/`)

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,10 @@ open target/site/jacoco/index.html
243243
- **Test Database** - SQLite in-memory (jdbc:sqlite::memory:) for fast, isolated test execution
244244
- **Mocking** - Mockito with `@MockitoBean` for dependency mocking
245245
- **Assertions** - AssertJ fluent assertions
246-
- **Naming Convention** - `method_scenario_outcome` pattern:
247-
- `getAll_playersExist_returnsOkWithAllPlayers()`
248-
- `post_squadNumberExists_returnsConflict()`
249-
- `findById_playerExists_returnsPlayer()`
246+
- **Naming Convention** - `givenX_whenY_thenZ` BDD pattern:
247+
- `givenPlayersExist_whenGetAll_thenReturnsOkWithAllPlayers()`
248+
- `givenSquadNumberExists_whenPost_thenReturnsConflict()`
249+
- `givenPlayerExists_whenFindById_thenReturnsPlayer()`
250250

251251
**Coverage Targets:**
252252

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
* <li><b>GET</b> {@code /players} - Retrieve all players</li>
4040
* <li><b>GET</b> {@code /players/{id}} - Retrieve player by ID</li>
4141
* <li><b>GET</b> {@code /players/search/league/{league}} - Search players by league name</li>
42-
* <li><b>GET</b> {@code /players/search/squadnumber/{number}} - Search player by squad number</li>
42+
* <li><b>GET</b> {@code /players/squadnumber/{number}} - Retrieve player by squad number</li>
4343
* <li><b>POST</b> {@code /players} - Create a new player</li>
4444
* <li><b>PUT</b> {@code /players/{id}} - Update an existing player</li>
4545
* <li><b>DELETE</b> {@code /players/{id}} - Delete a player by ID</li>
@@ -113,6 +113,24 @@ public ResponseEntity<Void> post(@RequestBody @Valid PlayerDTO playerDTO) {
113113
* -----------------------------------------------------------------------------------------------------------------------
114114
*/
115115

116+
/**
117+
* Retrieves all players in the squad.
118+
* <p>
119+
* Returns the complete Argentina 2022 FIFA World Cup squad (26 players).
120+
* </p>
121+
*
122+
* @return 200 OK with array of all players (empty array if none found)
123+
*/
124+
@GetMapping("/players")
125+
@Operation(summary = "Retrieves all players")
126+
@ApiResponses(value = {
127+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO[].class)))
128+
})
129+
public ResponseEntity<List<PlayerDTO>> getAll() {
130+
List<PlayerDTO> players = playersService.retrieveAll();
131+
return ResponseEntity.status(HttpStatus.OK).body(players);
132+
}
133+
116134
/**
117135
* Retrieves a single player by their unique identifier.
118136
*
@@ -133,21 +151,26 @@ public ResponseEntity<PlayerDTO> getById(@PathVariable Long id) {
133151
}
134152

135153
/**
136-
* Retrieves all players in the squad.
154+
* Retrieves a player by their squad number (unique identifier).
137155
* <p>
138-
* Returns the complete Argentina 2022 FIFA World Cup squad (26 players).
156+
* Squad numbers are unique jersey numbers (e.g., Messi is #10). This is a direct lookup similar to getById().
157+
* Example: {@code /players/squadnumber/10} returns Lionel Messi
139158
* </p>
140159
*
141-
* @return 200 OK with array of all players (empty array if none found)
160+
* @param squadNumber the squad number to retrieve (jersey number, typically 1-99)
161+
* @return 200 OK with player data, or 404 Not Found if no player has that number
142162
*/
143-
@GetMapping("/players")
144-
@Operation(summary = "Retrieves all players")
163+
@GetMapping("/players/squadnumber/{squadNumber}")
164+
@Operation(summary = "Retrieves a player by squad number")
145165
@ApiResponses(value = {
146-
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO[].class)))
166+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO.class))),
167+
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content)
147168
})
148-
public ResponseEntity<List<PlayerDTO>> getAll() {
149-
List<PlayerDTO> players = playersService.retrieveAll();
150-
return ResponseEntity.status(HttpStatus.OK).body(players);
169+
public ResponseEntity<PlayerDTO> getBySquadNumber(@PathVariable Integer squadNumber) {
170+
PlayerDTO player = playersService.retrieveBySquadNumber(squadNumber);
171+
return (player != null)
172+
? ResponseEntity.status(HttpStatus.OK).body(player)
173+
: ResponseEntity.status(HttpStatus.NOT_FOUND).build();
151174
}
152175

153176
/**
@@ -169,30 +192,6 @@ public ResponseEntity<List<PlayerDTO>> searchByLeague(@PathVariable String leagu
169192
return ResponseEntity.status(HttpStatus.OK).body(players);
170193
}
171194

172-
/**
173-
* Searches for a player by their squad number.
174-
* <p>
175-
* Squad numbers are jersey numbers that users recognize (e.g., Messi is #10).
176-
* Example: {@code /players/search/squadnumber/10} returns Lionel Messi
177-
* </p>
178-
*
179-
* @param squadNumber the squad number to search for (jersey number, typically 1-99)
180-
* @return 200 OK with player data, or 404 Not Found if no player has that
181-
* number
182-
*/
183-
@GetMapping("/players/search/squadnumber/{squadNumber}")
184-
@Operation(summary = "Searches for a player by squad number")
185-
@ApiResponses(value = {
186-
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO.class))),
187-
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content)
188-
})
189-
public ResponseEntity<PlayerDTO> searchBySquadNumber(@PathVariable Integer squadNumber) {
190-
PlayerDTO player = playersService.searchBySquadNumber(squadNumber);
191-
return (player != null)
192-
? ResponseEntity.status(HttpStatus.OK).body(player)
193-
: ResponseEntity.status(HttpStatus.NOT_FOUND).build();
194-
}
195-
196195
/*
197196
* -----------------------------------------------------------------------------------------------------------------------
198197
* HTTP PUT

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import java.util.Optional;
55

66
import org.springframework.data.jpa.repository.JpaRepository;
7-
import org.springframework.data.jpa.repository.Query;
8-
import org.springframework.data.repository.query.Param;
97
import org.springframework.stereotype.Repository;
108

119
import ar.com.nanotaboada.java.samples.spring.boot.models.Player;
@@ -26,8 +24,8 @@
2624
*
2725
* <h3>Query Strategies:</h3>
2826
* <ul>
29-
* <li><b>@Query:</b> Explicit JPQL for complex searches (findByLeagueContainingIgnoreCase)</li>
30-
* <li><b>Method Names:</b> Spring Data derives queries from method names (Query Creation)</li>
27+
* <li><b>Derived Queries:</b> Spring Data derives queries from method names (findBySquadNumber,
28+
* findByLeagueContainingIgnoreCase)</li>
3129
* </ul>
3230
*
3331
* @see Player
@@ -40,20 +38,6 @@
4038
@Repository
4139
public interface PlayersRepository extends JpaRepository<Player, Long> {
4240

43-
/**
44-
* Finds players by league name using case-insensitive wildcard matching.
45-
* <p>
46-
* This method uses a custom JPQL query with LIKE operator for partial matches.
47-
* For example, searching for "Premier" will match "Premier League".
48-
* </p>
49-
*
50-
* @param league the league name to search for (partial matches allowed)
51-
* @return a list of players whose league name contains the search term (empty
52-
* list if none found)
53-
*/
54-
@Query("SELECT p FROM Player p WHERE LOWER(p.league) LIKE LOWER(CONCAT('%', :league, '%'))")
55-
List<Player> findByLeagueContainingIgnoreCase(@Param("league") String league);
56-
5741
/**
5842
* Finds a player by their squad number (exact match).
5943
* <p>
@@ -67,4 +51,17 @@ public interface PlayersRepository extends JpaRepository<Player, Long> {
6751
* @return an Optional containing the player if found, empty Optional otherwise
6852
*/
6953
Optional<Player> findBySquadNumber(Integer squadNumber);
54+
55+
/**
56+
* Finds players by league name using case-insensitive wildcard matching.
57+
* <p>
58+
* This method uses Spring Data's derived query mechanism to perform partial matching.
59+
* For example, searching for "Premier" will match "Premier League".
60+
* </p>
61+
*
62+
* @param league the league name to search for (partial matches allowed)
63+
* @return a list of players whose league name contains the search term (empty
64+
* list if none found)
65+
*/
66+
List<Player> findByLeagueContainingIgnoreCase(String league);
7067
}

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,24 @@ public PlayerDTO create(PlayerDTO playerDTO) {
110110
* -----------------------------------------------------------------------------------------------------------------------
111111
*/
112112

113+
/**
114+
* Retrieves all players from the database.
115+
* <p>
116+
* This method returns the complete Argentina 2022 FIFA World Cup squad (26 players).
117+
* Results are cached to improve performance on subsequent calls.
118+
* </p>
119+
*
120+
* @return a list of all players (empty list if none found)
121+
* @see org.springframework.cache.annotation.Cacheable
122+
*/
123+
@Cacheable(value = "players")
124+
public List<PlayerDTO> retrieveAll() {
125+
return playersRepository.findAll()
126+
.stream()
127+
.map(this::mapFrom)
128+
.toList();
129+
}
130+
113131
/**
114132
* Retrieves a player by their unique identifier.
115133
* <p>
@@ -130,21 +148,21 @@ public PlayerDTO retrieveById(Long id) {
130148
}
131149

132150
/**
133-
* Retrieves all players from the database.
151+
* Retrieves a player by their squad number (unique identifier).
134152
* <p>
135-
* This method returns the complete Argentina 2022 FIFA World Cup squad (26 players).
136-
* Results are cached to improve performance on subsequent calls.
153+
* Squad numbers are unique jersey numbers (e.g., Messi is #10). This is a direct lookup by unique identifier,
154+
* similar to retrieveById(). Results are cached to improve performance.
137155
* </p>
138156
*
139-
* @return a list of all players (empty list if none found)
157+
* @param squadNumber the squad number to retrieve (jersey number, typically 1-99)
158+
* @return the player DTO if found, null otherwise
140159
* @see org.springframework.cache.annotation.Cacheable
141160
*/
142-
@Cacheable(value = "players")
143-
public List<PlayerDTO> retrieveAll() {
144-
return playersRepository.findAll()
145-
.stream()
161+
@Cacheable(value = "players", key = "'squad-' + #squadNumber", unless = "#result == null")
162+
public PlayerDTO retrieveBySquadNumber(Integer squadNumber) {
163+
return playersRepository.findBySquadNumber(squadNumber)
146164
.map(this::mapFrom)
147-
.toList();
165+
.orElse(null);
148166
}
149167

150168
/*
@@ -170,22 +188,6 @@ public List<PlayerDTO> searchByLeague(String league) {
170188
.toList();
171189
}
172190

173-
/**
174-
* Searches for a player by their squad number.
175-
* <p>
176-
* This method performs an exact match on the squad number field. Squad numbers are jersey numbers that users recognize
177-
* (e.g., Messi is #10).
178-
* </p>
179-
*
180-
* @param squadNumber the squad number to search for (jersey number, typically 1-99)
181-
* @return the player DTO if found, null otherwise
182-
*/
183-
public PlayerDTO searchBySquadNumber(Integer squadNumber) {
184-
return playersRepository.findBySquadNumber(squadNumber)
185-
.map(this::mapFrom)
186-
.orElse(null);
187-
}
188-
189191
/*
190192
* -----------------------------------------------------------------------------------------------------------------------
191193
* Update

0 commit comments

Comments
 (0)