Skip to content
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 73 additions & 102 deletions src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
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;
import org.mockito.junit.jupiter.MockitoExtension;
Expand All @@ -21,6 +24,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;
Expand Down Expand Up @@ -120,78 +124,50 @@ 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);

// Act: call the service method we are testing
Book result = testBookService.createBook(validBookRequest);
Book expectedBook =
Book.builder()
.id(testId)
.title(specialRequest.title())
.synopsis(specialRequest.synopsis())
.author(specialRequest.author())
.build();

// 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());
when(testBookRepository.save(any(Book.class))).thenReturn(expectedBook);

// Did the service perform the correct action on its dependency?
verify(testBookRepository, times(1)).save(any(Book.class));
}
// Act
Book result = testBookService.createBook(specialRequest);

@Test
void testCreateBook_NullRequest_ThrowsException() {
// Act & Assert
assertThrows(
NullPointerException.class,
() -> {
testBookService.createBook(null);
}
);
}
// Assert: Verify the Service fulfills its return contract
assertEquals(expectedBook, result, "Service must return the object returned by the repository");

@Test
void testCreateBook_NullTitle_ThrowsException() {
// Arrange
BookRequest invalidRequest = new BookRequest(null, "Synopsis", "Author");
// Assert: capture the Book passed to save()
ArgumentCaptor<Book> bookCaptor = ArgumentCaptor.forClass(Book.class);
verify(testBookRepository, times(1)).save(bookCaptor.capture());
Book savedBook = bookCaptor.getValue();

// Act & Assert
assertThrows(
IllegalArgumentException.class,
() -> {
testBookService.createBook(invalidRequest);
}
);
// 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());

// 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");

void testCreateBook_NullRequest_ThrowsException() {
// Act & Assert
assertThrows(
IllegalArgumentException.class,
NullPointerException.class,
() -> {
testBookService.createBook(invalidRequest);
testBookService.createBook(null);
}
);
}
Expand All @@ -211,6 +187,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
Expand All @@ -227,44 +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));
}

@Test
void testCreateBook_SpecialCharactersInTitle_Success() {
// Arrange
BookRequest specialRequest =
new BookRequest("Test: A Book! @#$%^&*()", "Synopsis", "Author");
// --- CAPTOR LOGIC ---
ArgumentCaptor<Book> bookCaptor = ArgumentCaptor.forClass(Book.class);
verify(testBookRepository).save(bookCaptor.capture());
Book bookSentToDb = bookCaptor.getValue();

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
assertNotNull(result);
assertEquals(testId, result.getId());
assertEquals(specialRequest.title(), result.getTitle());
assertEquals(specialRequest.synopsis(), result.getSynopsis());
assertEquals(specialRequest.author(), result.getAuthor());

// Did the service perform the correct action on its dependency?
verify(testBookRepository, times(1)).save(any(Book.class));
// 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());
}

// --------------------------------------------------------------------------------------------
Expand All @@ -274,13 +241,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)
);
Expand All @@ -293,24 +258,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<Book> 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
Expand Down
Loading