From 2f191dbf39b5f4a7fb7a4adee819233c76ced602 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 20 Nov 2025 11:11:39 +0000 Subject: [PATCH 1/7] Clean comments; Refactor to use ArgumentCaptor --- .../hmcts/bookapi/BookServiceTest.java | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java index 3d4fa45..bc491f7 100644 --- a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -21,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -256,15 +258,21 @@ void testCreateBook_SpecialCharactersInTitle_Success() { // Act Book result = testBookService.createBook(specialRequest); + // Assert: capture the Book passed to save() + ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); + verify(testBookRepository, times(1)).save(bookCaptor.capture()); + Book savedBook = bookCaptor.getValue(); + // Assert - assertNotNull(result); - assertEquals(testId, result.getId()); - assertEquals(specialRequest.title(), result.getTitle()); - assertEquals(specialRequest.synopsis(), result.getSynopsis()); - assertEquals(specialRequest.author(), result.getAuthor()); + assertNotNull(savedBook); + assertNull(savedBook.getId(), "ID should be null before DB generates it"); + assertEquals(specialRequest.title(), savedBook.getTitle()); + assertEquals(specialRequest.synopsis(), savedBook.getSynopsis()); + assertEquals(specialRequest.author(), savedBook.getAuthor()); + + // Assert: Verify the Service fulfills its return contract + assertEquals(expectedBook, result, "Service must return the object returned by the repository"); - // Did the service perform the correct action on its dependency? - verify(testBookRepository, times(1)).save(any(Book.class)); } // -------------------------------------------------------------------------------------------- @@ -274,13 +282,11 @@ void testCreateBook_SpecialCharactersInTitle_Success() { @Test void testDelete_Book_ShouldThrowException_WhenIdNotFound() { - // Arrange: As goal is to test what happens when the resource doesn't exist, - // we intentionally simulate DB returning NO result + // Arrange: simulate missing record when(testBookRepository.findById(testId)).thenReturn(Optional.empty()); - // ACT and ASSERT: throw ResourceNotFoundException when calling the delete method. + // ACT and ASSERT: correct exception thrown assertThrows( - // custom exception to reflect business rules vs technical problem ResourceNotFoundException.class, () -> testBookService.deleteBookById(testId) ); @@ -293,24 +299,30 @@ void testDelete_Book_ShouldThrowException_WhenIdNotFound() { @Test void testDeleteBookById_Success() { - // Arrange: - persistedBook.setDeleted(false); // ensure starting state + // Arrange: ensure the test book is active + persistedBook.setDeleted(false); when(testBookRepository.findById(testId)) .thenReturn(Optional.of(persistedBook)); - when(testBookRepository.save(any(Book.class))) - .thenReturn(persistedBook); - // Act: call the service method we are testing testBookService.deleteBookById(testId); // Assert: the entity was marked deleted - assertTrue(persistedBook.isDeleted()); + ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); + verify(testBookRepository, times(1)).save(bookCaptor.capture()); + + // extract from captured object (internal list) + Book savedBook = bookCaptor.getValue(); + + // Assert: the service correctly marked it as deleted + assertTrue( + savedBook.isDeleted(), + "Book passed to save() should be marked deleted" + ); + // Assert: it is the same ID we attempted to delete + assertEquals(testId, savedBook.getId()); - // Assert: repository methods were called correctly - verify(testBookRepository, times(1)).findById(testId); - verify(testBookRepository, times(1)).save(persistedBook); } @Test From a2317c615c9123632de03a8f197d4d6e7eeccbc7 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 20 Nov 2025 11:16:56 +0000 Subject: [PATCH 2/7] Update success test to use ArgumentCaptor + special chars (extra redundant test) --- .../hmcts/bookapi/BookServiceTest.java | 78 +++++++------------ 1 file changed, 30 insertions(+), 48 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java index bc491f7..c557b9e 100644 --- a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java @@ -122,26 +122,44 @@ void setUp() { // -------------------------------------- // Tests: createBook // -------------------------------------- + @Test void testCreateBook_Success() { + // Arrange: also checks special chars + BookRequest specialRequest = + new BookRequest("Java & Friends!", "Synopsis", "Author"); - // Arrange: tell the mock repository what to do when called - when(testBookRepository.save(any(Book.class))).thenReturn(persistedBook); + Book expectedBook = + Book.builder() + .id(testId) + .title(specialRequest.title()) + .synopsis(specialRequest.synopsis()) + .author(specialRequest.author()) + .build(); - // Act: call the service method we are testing - Book result = testBookService.createBook(validBookRequest); + when(testBookRepository.save(any(Book.class))).thenReturn(expectedBook); - // Assert: Check the outcome - assertNotNull(result); - assertEquals(testId, result.getId()); - assertEquals(validBookRequest.title(), result.getTitle()); - assertEquals(validBookRequest.synopsis(), result.getSynopsis()); - assertEquals(validBookRequest.author(), result.getAuthor()); + // Act + Book result = testBookService.createBook(specialRequest); + + // Assert: capture the Book passed to save() + ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); + verify(testBookRepository, times(1)).save(bookCaptor.capture()); + Book savedBook = bookCaptor.getValue(); + + // Assert + assertNotNull(savedBook); + assertNull(savedBook.getId(), "ID should be null before DB generates it"); + assertEquals(specialRequest.title(), savedBook.getTitle()); + assertEquals(specialRequest.synopsis(), savedBook.getSynopsis()); + assertEquals(specialRequest.author(), savedBook.getAuthor()); + + // Assert: Verify the Service fulfills its return contract + assertEquals(expectedBook, result, "Service must return the object returned by the repository"); - // Did the service perform the correct action on its dependency? - verify(testBookRepository, times(1)).save(any(Book.class)); } + @Test void testCreateBook_NullRequest_ThrowsException() { // Act & Assert @@ -239,42 +257,6 @@ void testCreateBook_VeryLongFields_Success( verify(testBookRepository, times(1)).save(any(Book.class)); } - @Test - void testCreateBook_SpecialCharactersInTitle_Success() { - // Arrange - BookRequest specialRequest = - new BookRequest("Test: A Book! @#$%^&*()", "Synopsis", "Author"); - - Book expectedBook = - Book.builder() - .id(testId) - .title(specialRequest.title()) - .synopsis(specialRequest.synopsis()) - .author(specialRequest.author()) - .build(); - - when(testBookRepository.save(any(Book.class))).thenReturn(expectedBook); - - // Act - Book result = testBookService.createBook(specialRequest); - - // Assert: capture the Book passed to save() - ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); - verify(testBookRepository, times(1)).save(bookCaptor.capture()); - Book savedBook = bookCaptor.getValue(); - - // Assert - assertNotNull(savedBook); - assertNull(savedBook.getId(), "ID should be null before DB generates it"); - assertEquals(specialRequest.title(), savedBook.getTitle()); - assertEquals(specialRequest.synopsis(), savedBook.getSynopsis()); - assertEquals(specialRequest.author(), savedBook.getAuthor()); - - // Assert: Verify the Service fulfills its return contract - assertEquals(expectedBook, result, "Service must return the object returned by the repository"); - - } - // -------------------------------------------------------------------------------------------- // Tests: deleteBookById(UUID) // ------------------------------------------------------------------------------------------- From e6fe6077dd1438257b61996ae65ea8697373694b Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 20 Nov 2025 11:24:35 +0000 Subject: [PATCH 3/7] Combine 'Bad title' tests to @ParameterizedTest + @nullAndEmptySource --- .../hmcts/bookapi/BookServiceTest.java | 60 +++++-------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java index c557b9e..0f61e84 100644 --- a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java @@ -11,6 +11,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -171,51 +173,6 @@ void testCreateBook_NullRequest_ThrowsException() { ); } - @Test - void testCreateBook_NullTitle_ThrowsException() { - // Arrange - BookRequest invalidRequest = new BookRequest(null, "Synopsis", "Author"); - - // Act & Assert - assertThrows( - IllegalArgumentException.class, - () -> { - testBookService.createBook(invalidRequest); - } - ); - - // Verify repository was never called - verify(testBookRepository, never()).save(any()); - } - - @Test - void testCreateBook_EmptyTitle_ThrowsException() { - // Arrange - BookRequest invalidRequest = new BookRequest("", "Synopsis", "Author"); - - // Act & Assert - assertThrows( - IllegalArgumentException.class, - () -> { - testBookService.createBook(invalidRequest); - } - ); - } - - @Test - void testCreateBook_BlankTitle_ThrowsException() { - // Arrange - BookRequest invalidRequest = new BookRequest(" ", "Synopsis", "Author"); - - // Act & Assert - assertThrows( - IllegalArgumentException.class, - () -> { - testBookService.createBook(invalidRequest); - } - ); - } - @Test void testCreateBook_RepositoryFailure_ThrowsException() { // Arrange @@ -231,6 +188,19 @@ void testCreateBook_RepositoryFailure_ThrowsException() { ); } + @ParameterizedTest + @NullAndEmptySource // Covers Null and "" (Empty) + @ValueSource(strings = {" ", "\t", "\n"}) // Covers Blank (Spaces/Tabs) + void testCreateBook_InvalidTitle_ThrowsException(String invalidTitle) { + // Arrange + BookRequest invalidRequest = new BookRequest(invalidTitle, "Synopsis", "Author"); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> { + testBookService.createBook(invalidRequest); + }); + } + // ----- EDGE cases --------- @ParameterizedTest(name = "{0}") // Display the test name From c453a3c8026244ca516c4f4e33214062d428e676 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 20 Nov 2025 11:27:58 +0000 Subject: [PATCH 4/7] Fix checkstyle warnings --- .../com/codesungrape/hmcts/bookapi/BookServiceTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java index 0f61e84..11a20fb 100644 --- a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java @@ -144,6 +144,9 @@ void testCreateBook_Success() { // Act Book result = testBookService.createBook(specialRequest); + // Assert: Verify the Service fulfills its return contract + assertEquals(expectedBook, result, "Service must return the object returned by the repository"); + // Assert: capture the Book passed to save() ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); verify(testBookRepository, times(1)).save(bookCaptor.capture()); @@ -156,12 +159,8 @@ void testCreateBook_Success() { assertEquals(specialRequest.synopsis(), savedBook.getSynopsis()); assertEquals(specialRequest.author(), savedBook.getAuthor()); - // Assert: Verify the Service fulfills its return contract - assertEquals(expectedBook, result, "Service must return the object returned by the repository"); - } - @Test void testCreateBook_NullRequest_ThrowsException() { // Act & Assert From 801af16ed25bbc4b79a16a81f99b93eaae66b98a Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 20 Nov 2025 11:57:56 +0000 Subject: [PATCH 5/7] improve assertion safety with ArgumentCaptor --- .../hmcts/bookapi/BookServiceTest.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java index 11a20fb..142981a 100644 --- a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java @@ -216,14 +216,22 @@ void testCreateBook_VeryLongFields_Success( // Act Book result = testBookService.createBook(request); - // Assert - assertNotNull(result); + // Assert 1: Verify the result flow + assertEquals(expectedBook, result); assertEquals(expectedBook.getId(), result.getId()); assertEquals(expectedBook.getTitle(), result.getTitle()); assertEquals(expectedBook.getSynopsis(), result.getSynopsis()); assertEquals(expectedBook.getAuthor(), result.getAuthor()); - verify(testBookRepository, times(1)).save(any(Book.class)); + // --- CAPTOR LOGIC --- + ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); + verify(testBookRepository).save(bookCaptor.capture()); + Book bookSentToDb = bookCaptor.getValue(); + + // Assert 2: Verify the Service correctly mapped the LONG strings + // This ensures the service didn't truncate/alter the data + assertEquals(request.title(), bookSentToDb.getTitle()); + assertEquals(request.synopsis(), bookSentToDb.getSynopsis()); } // -------------------------------------------------------------------------------------------- From 686647db0b4d89312933fe2d4f78cf50cb156c29 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 20 Nov 2025 12:06:45 +0000 Subject: [PATCH 6/7] remove redudnant asserts, fix typo --- .../com/codesungrape/hmcts/bookapi/BookServiceTest.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java index 142981a..3fa1650 100644 --- a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java @@ -209,7 +209,6 @@ void testCreateBook_VeryLongFields_Success( BookRequest request, Book expectedBook ) { - // Arrange when(testBookRepository.save(any(Book.class))).thenReturn(expectedBook); @@ -218,10 +217,6 @@ void testCreateBook_VeryLongFields_Success( // Assert 1: Verify the result flow assertEquals(expectedBook, result); - assertEquals(expectedBook.getId(), result.getId()); - assertEquals(expectedBook.getTitle(), result.getTitle()); - assertEquals(expectedBook.getSynopsis(), result.getSynopsis()); - assertEquals(expectedBook.getAuthor(), result.getAuthor()); // --- CAPTOR LOGIC --- ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); @@ -271,7 +266,7 @@ void testDeleteBookById_Success() { ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); verify(testBookRepository, times(1)).save(bookCaptor.capture()); - // extract from captured object (internal list) + // extract from the captured object (internal list) Book savedBook = bookCaptor.getValue(); // Assert: the service correctly marked it as deleted From f11f225c24c5388db425e2660e04c487e07a9ec8 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 20 Nov 2025 12:42:32 +0000 Subject: [PATCH 7/7] Rename variables for better clarity for others --- .../hmcts/bookapi/BookServiceTest.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java index 3fa1650..a1506e0 100644 --- a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java @@ -23,7 +23,6 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -131,7 +130,8 @@ void testCreateBook_Success() { BookRequest specialRequest = new BookRequest("Java & Friends!", "Synopsis", "Author"); - Book expectedBook = + // The "Future" Book (What the DB returns) + Book bookFromDb = Book.builder() .id(testId) .title(specialRequest.title()) @@ -139,26 +139,24 @@ void testCreateBook_Success() { .author(specialRequest.author()) .build(); - when(testBookRepository.save(any(Book.class))).thenReturn(expectedBook); + when(testBookRepository.save(any(Book.class))).thenReturn(bookFromDb); // Act Book result = testBookService.createBook(specialRequest); // Assert: Verify the Service fulfills its return contract - assertEquals(expectedBook, result, "Service must return the object returned by the repository"); + assertEquals(bookFromDb, result, "Service must return the object returned by the repository"); - // Assert: capture the Book passed to save() + // Assert: Capture the "Past" Book (What went INTO the DB) ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); verify(testBookRepository, times(1)).save(bookCaptor.capture()); - Book savedBook = bookCaptor.getValue(); + Book bookSentToDb = bookCaptor.getValue(); // Assert - assertNotNull(savedBook); - assertNull(savedBook.getId(), "ID should be null before DB generates it"); - assertEquals(specialRequest.title(), savedBook.getTitle()); - assertEquals(specialRequest.synopsis(), savedBook.getSynopsis()); - assertEquals(specialRequest.author(), savedBook.getAuthor()); - + assertNull(bookSentToDb.getId(), "ID should be null before DB generates it"); + assertEquals(specialRequest.title(), bookSentToDb.getTitle()); + assertEquals(specialRequest.synopsis(), bookSentToDb.getSynopsis()); + assertEquals(specialRequest.author(), bookSentToDb.getAuthor()); } @Test