1111import org .junit .jupiter .params .ParameterizedTest ;
1212import org .junit .jupiter .params .provider .Arguments ;
1313import org .junit .jupiter .params .provider .MethodSource ;
14+ import org .junit .jupiter .params .provider .NullAndEmptySource ;
15+ import org .junit .jupiter .params .provider .ValueSource ;
16+ import org .mockito .ArgumentCaptor ;
1417import org .mockito .InjectMocks ;
1518import org .mockito .Mock ;
1619import org .mockito .junit .jupiter .MockitoExtension ;
2023import java .util .stream .Stream ;
2124
2225import static org .junit .jupiter .api .Assertions .assertEquals ;
23- import static org .junit .jupiter .api .Assertions .assertNotNull ;
26+ import static org .junit .jupiter .api .Assertions .assertNull ;
2427import static org .junit .jupiter .api .Assertions .assertThrows ;
2528import static org .junit .jupiter .api .Assertions .assertTrue ;
2629import static org .mockito .ArgumentMatchers .any ;
@@ -120,24 +123,40 @@ void setUp() {
120123 // --------------------------------------
121124 // Tests: createBook
122125 // --------------------------------------
126+
123127 @ Test
124128 void testCreateBook_Success () {
129+ // Arrange: also checks special chars
130+ BookRequest specialRequest =
131+ new BookRequest ("Java & Friends!" , "Synopsis" , "Author" );
125132
126- // Arrange: tell the mock repository what to do when called
127- when (testBookRepository .save (any (Book .class ))).thenReturn (persistedBook );
133+ // The "Future" Book (What the DB returns)
134+ Book bookFromDb =
135+ Book .builder ()
136+ .id (testId )
137+ .title (specialRequest .title ())
138+ .synopsis (specialRequest .synopsis ())
139+ .author (specialRequest .author ())
140+ .build ();
128141
129- // Act: call the service method we are testing
130- Book result = testBookService .createBook (validBookRequest );
142+ when (testBookRepository .save (any (Book .class ))).thenReturn (bookFromDb );
131143
132- // Assert: Check the outcome
133- assertNotNull (result );
134- assertEquals (testId , result .getId ());
135- assertEquals (validBookRequest .title (), result .getTitle ());
136- assertEquals (validBookRequest .synopsis (), result .getSynopsis ());
137- assertEquals (validBookRequest .author (), result .getAuthor ());
144+ // Act
145+ Book result = testBookService .createBook (specialRequest );
146+
147+ // Assert: Verify the Service fulfills its return contract
148+ assertEquals (bookFromDb , result , "Service must return the object returned by the repository" );
149+
150+ // Assert: Capture the "Past" Book (What went INTO the DB)
151+ ArgumentCaptor <Book > bookCaptor = ArgumentCaptor .forClass (Book .class );
152+ verify (testBookRepository , times (1 )).save (bookCaptor .capture ());
153+ Book bookSentToDb = bookCaptor .getValue ();
138154
139- // Did the service perform the correct action on its dependency?
140- verify (testBookRepository , times (1 )).save (any (Book .class ));
155+ // Assert
156+ assertNull (bookSentToDb .getId (), "ID should be null before DB generates it" );
157+ assertEquals (specialRequest .title (), bookSentToDb .getTitle ());
158+ assertEquals (specialRequest .synopsis (), bookSentToDb .getSynopsis ());
159+ assertEquals (specialRequest .author (), bookSentToDb .getAuthor ());
141160 }
142161
143162 @ Test
@@ -151,51 +170,6 @@ void testCreateBook_NullRequest_ThrowsException() {
151170 );
152171 }
153172
154- @ Test
155- void testCreateBook_NullTitle_ThrowsException () {
156- // Arrange
157- BookRequest invalidRequest = new BookRequest (null , "Synopsis" , "Author" );
158-
159- // Act & Assert
160- assertThrows (
161- IllegalArgumentException .class ,
162- () -> {
163- testBookService .createBook (invalidRequest );
164- }
165- );
166-
167- // Verify repository was never called
168- verify (testBookRepository , never ()).save (any ());
169- }
170-
171- @ Test
172- void testCreateBook_EmptyTitle_ThrowsException () {
173- // Arrange
174- BookRequest invalidRequest = new BookRequest ("" , "Synopsis" , "Author" );
175-
176- // Act & Assert
177- assertThrows (
178- IllegalArgumentException .class ,
179- () -> {
180- testBookService .createBook (invalidRequest );
181- }
182- );
183- }
184-
185- @ Test
186- void testCreateBook_BlankTitle_ThrowsException () {
187- // Arrange
188- BookRequest invalidRequest = new BookRequest (" " , "Synopsis" , "Author" );
189-
190- // Act & Assert
191- assertThrows (
192- IllegalArgumentException .class ,
193- () -> {
194- testBookService .createBook (invalidRequest );
195- }
196- );
197- }
198-
199173 @ Test
200174 void testCreateBook_RepositoryFailure_ThrowsException () {
201175 // Arrange
@@ -211,6 +185,19 @@ void testCreateBook_RepositoryFailure_ThrowsException() {
211185 );
212186 }
213187
188+ @ ParameterizedTest
189+ @ NullAndEmptySource // Covers Null and "" (Empty)
190+ @ ValueSource (strings = {" " , "\t " , "\n " }) // Covers Blank (Spaces/Tabs)
191+ void testCreateBook_InvalidTitle_ThrowsException (String invalidTitle ) {
192+ // Arrange
193+ BookRequest invalidRequest = new BookRequest (invalidTitle , "Synopsis" , "Author" );
194+
195+ // Act & Assert
196+ assertThrows (IllegalArgumentException .class , () -> {
197+ testBookService .createBook (invalidRequest );
198+ });
199+ }
200+
214201 // ----- EDGE cases ---------
215202
216203 @ ParameterizedTest (name = "{0}" ) // Display the test name
@@ -220,51 +207,24 @@ void testCreateBook_VeryLongFields_Success(
220207 BookRequest request ,
221208 Book expectedBook
222209 ) {
223-
224210 // Arrange
225211 when (testBookRepository .save (any (Book .class ))).thenReturn (expectedBook );
226212
227213 // Act
228214 Book result = testBookService .createBook (request );
229215
230- // Assert
231- assertNotNull (result );
232- assertEquals (expectedBook .getId (), result .getId ());
233- assertEquals (expectedBook .getTitle (), result .getTitle ());
234- assertEquals (expectedBook .getSynopsis (), result .getSynopsis ());
235- assertEquals (expectedBook .getAuthor (), result .getAuthor ());
236-
237- verify (testBookRepository , times (1 )).save (any (Book .class ));
238- }
239-
240- @ Test
241- void testCreateBook_SpecialCharactersInTitle_Success () {
242- // Arrange
243- BookRequest specialRequest =
244- new BookRequest ("Test: A Book! @#$%^&*()" , "Synopsis" , "Author" );
245-
246- Book expectedBook =
247- Book .builder ()
248- .id (testId )
249- .title (specialRequest .title ())
250- .synopsis (specialRequest .synopsis ())
251- .author (specialRequest .author ())
252- .build ();
216+ // Assert 1: Verify the result flow
217+ assertEquals (expectedBook , result );
253218
254- when (testBookRepository .save (any (Book .class ))).thenReturn (expectedBook );
219+ // --- CAPTOR LOGIC ---
220+ ArgumentCaptor <Book > bookCaptor = ArgumentCaptor .forClass (Book .class );
221+ verify (testBookRepository ).save (bookCaptor .capture ());
222+ Book bookSentToDb = bookCaptor .getValue ();
255223
256- // Act
257- Book result = testBookService .createBook (specialRequest );
258-
259- // Assert
260- assertNotNull (result );
261- assertEquals (testId , result .getId ());
262- assertEquals (specialRequest .title (), result .getTitle ());
263- assertEquals (specialRequest .synopsis (), result .getSynopsis ());
264- assertEquals (specialRequest .author (), result .getAuthor ());
265-
266- // Did the service perform the correct action on its dependency?
267- verify (testBookRepository , times (1 )).save (any (Book .class ));
224+ // Assert 2: Verify the Service correctly mapped the LONG strings
225+ // This ensures the service didn't truncate/alter the data
226+ assertEquals (request .title (), bookSentToDb .getTitle ());
227+ assertEquals (request .synopsis (), bookSentToDb .getSynopsis ());
268228 }
269229
270230 // --------------------------------------------------------------------------------------------
@@ -274,13 +234,11 @@ void testCreateBook_SpecialCharactersInTitle_Success() {
274234 @ Test
275235 void testDelete_Book_ShouldThrowException_WhenIdNotFound () {
276236
277- // Arrange: As goal is to test what happens when the resource doesn't exist,
278- // we intentionally simulate DB returning NO result
237+ // Arrange: simulate missing record
279238 when (testBookRepository .findById (testId )).thenReturn (Optional .empty ());
280239
281- // ACT and ASSERT: throw ResourceNotFoundException when calling the delete method.
240+ // ACT and ASSERT: correct exception thrown
282241 assertThrows (
283- // custom exception to reflect business rules vs technical problem
284242 ResourceNotFoundException .class ,
285243 () -> testBookService .deleteBookById (testId )
286244 );
@@ -293,24 +251,30 @@ void testDelete_Book_ShouldThrowException_WhenIdNotFound() {
293251 @ Test
294252 void testDeleteBookById_Success () {
295253
296- // Arrange:
297- persistedBook .setDeleted (false ); // ensure starting state
254+ // Arrange: ensure the test book is active
255+ persistedBook .setDeleted (false );
298256
299257 when (testBookRepository .findById (testId ))
300258 .thenReturn (Optional .of (persistedBook ));
301259
302- when (testBookRepository .save (any (Book .class )))
303- .thenReturn (persistedBook );
304-
305260 // Act: call the service method we are testing
306261 testBookService .deleteBookById (testId );
307262
308263 // Assert: the entity was marked deleted
309- assertTrue (persistedBook .isDeleted ());
264+ ArgumentCaptor <Book > bookCaptor = ArgumentCaptor .forClass (Book .class );
265+ verify (testBookRepository , times (1 )).save (bookCaptor .capture ());
266+
267+ // extract from the captured object (internal list)
268+ Book savedBook = bookCaptor .getValue ();
269+
270+ // Assert: the service correctly marked it as deleted
271+ assertTrue (
272+ savedBook .isDeleted (),
273+ "Book passed to save() should be marked deleted"
274+ );
275+ // Assert: it is the same ID we attempted to delete
276+ assertEquals (testId , savedBook .getId ());
310277
311- // Assert: repository methods were called correctly
312- verify (testBookRepository , times (1 )).findById (testId );
313- verify (testBookRepository , times (1 )).save (persistedBook );
314278 }
315279
316280 @ Test
0 commit comments