From 847639ea4de33fe93a623726f3a87eea9588fd08 Mon Sep 17 00:00:00 2001 From: Egor Date: Sat, 13 Sep 2025 02:20:07 +0200 Subject: [PATCH 1/4] - Added unit tests for BookRepository and CategoryRepository - Implemented service layer tests for BookService and CategoryService - Added controller tests for BookController and CategoryController with security mocking - Achieved over 50% test coverage for targeted classes - Included test coverage screenshot in PR description - Configured test environment with H2 database and security mocks - Added necessary test dependencies and configuration files Tests cover: - CRUD operations for both Book and Category entities - Security constraints and role-based access control - Pagination and search functionality - Validation and error handling scenarios --- pom.xml | 47 ++- .../bookrepo/config/CustomMySqlContainer.java | 33 ++ .../controller/BookControllerTest.java | 281 +++++++++++++++++ .../controller/CategoryControllerTest.java | 185 ++++++++++++ .../repository/BookRepositoryTest.java | 55 ++++ .../BookSpecificationBuilderTest.java | 112 +++++++ .../BookSpecificationProviderManagerTest.java | 87 ++++++ .../bookrepo/service/BookServiceTest.java | 283 ++++++++++++++++++ .../bookrepo/service/CategoryServiceTest.java | 152 ++++++++++ .../resources/application-test.properties | 5 +- 10 files changed, 1227 insertions(+), 13 deletions(-) create mode 100644 src/test/java/bookrepo/config/CustomMySqlContainer.java create mode 100644 src/test/java/bookrepo/controller/BookControllerTest.java create mode 100644 src/test/java/bookrepo/controller/CategoryControllerTest.java create mode 100644 src/test/java/bookrepo/repository/BookRepositoryTest.java create mode 100644 src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java create mode 100644 src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java create mode 100644 src/test/java/bookrepo/service/BookServiceTest.java create mode 100644 src/test/java/bookrepo/service/CategoryServiceTest.java diff --git a/pom.xml b/pom.xml index 1971709..0f766ca 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ 0.2.0 1.5.5.Final 0.11.5 + 1.19.0 @@ -73,11 +74,6 @@ org.springframework.boot spring-boot-starter-validation - - com.h2database - h2 - test - org.liquibase liquibase-core @@ -105,24 +101,57 @@ jjwt-api ${jjwt.version} - io.jsonwebtoken jjwt-impl ${jjwt.version} runtime - io.jsonwebtoken jjwt-jackson ${jjwt.version} runtime - + + org.springframework.data + spring-data-jdbc + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mysql + test + + + mysql + mysql-connector-java + 8.0.33 + + + org.springframework.security + spring-security-test + test + - + + + + org.testcontainers + testcontainers-bom + 1.21.3 + pom + import + + + + + org.springframework.boot diff --git a/src/test/java/bookrepo/config/CustomMySqlContainer.java b/src/test/java/bookrepo/config/CustomMySqlContainer.java new file mode 100644 index 0000000..93f22c2 --- /dev/null +++ b/src/test/java/bookrepo/config/CustomMySqlContainer.java @@ -0,0 +1,33 @@ +package bookrepo.config; + +import org.testcontainers.containers.MySQLContainer; + +public class CustomMySqlContainer extends MySQLContainer { + private static final String DB_IMAGE = "mysql:8"; + + private static CustomMySqlContainer mysqlContainer; + + private CustomMySqlContainer() { + super(DB_IMAGE); + } + + public static synchronized CustomMySqlContainer getInstance() { + if (mysqlContainer == null) { + mysqlContainer = new CustomMySqlContainer(); + } + return mysqlContainer; + } + + @Override + public void start() { + super.start(); + System.setProperty("TEST_DB_URL", mysqlContainer.getJdbcUrl()); + System.setProperty("TEST_DB_USERNAME", mysqlContainer.getUsername()); + System.setProperty("TEST_DB_PASSWORD", mysqlContainer.getPassword()); + } + + @Override + public void stop() { + } +} + diff --git a/src/test/java/bookrepo/controller/BookControllerTest.java b/src/test/java/bookrepo/controller/BookControllerTest.java new file mode 100644 index 0000000..b872de9 --- /dev/null +++ b/src/test/java/bookrepo/controller/BookControllerTest.java @@ -0,0 +1,281 @@ +package bookrepo.controller; + +import bookrepo.dto.book.BookDto; +import bookrepo.dto.book.BookDtoWithoutCategoryIds; +import bookrepo.dto.book.BookSearchParameters; +import bookrepo.dto.book.CreateBookRequestDto; +import bookrepo.service.BookService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class BookControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private BookService bookService; + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get all books") + void findAll_WithUserRole_ShouldReturnPageOfBooks() throws Exception { + // Given + BookDto bookDto = createBookDto(); + Page page = new PageImpl<>(List.of(bookDto)); + when(bookService.findAll(any(Pageable.class))).thenReturn(page); + + // When + MvcResult result = mockMvc.perform(get("/books") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andReturn(); + + String responseContent = result.getResponse().getContentAsString(); + + Assertions.assertTrue(responseContent.contains("Test Book")); + Assertions.assertTrue(responseContent.contains("Test Author")); + Assertions.assertTrue(responseContent.contains("29.99")); + + Assertions.assertTrue(responseContent.contains("\"title\":\"Test Book\"")); + Assertions.assertTrue(responseContent.contains("\"author\":\"Test Author\"")); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get book by ID") + void getBookById_WithUserRole_ShouldReturnBook() throws Exception { + // Given + BookDto expected = createBookDto(); + when(bookService.getById(1L)).thenReturn(expected); + + // When + MvcResult result = mockMvc.perform(get("/books/1")) + .andExpect(status().isOk()) + .andReturn(); + + // Then + BookDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto.class); + Assertions.assertNotNull(actual); + Assertions.assertEquals(expected.getId(), actual.getId()); + Assertions.assertEquals(expected.getTitle(), actual.getTitle()); + Assertions.assertEquals(expected.getAuthor(), actual.getAuthor()); + Assertions.assertEquals(expected.getPrice(), actual.getPrice()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Search books") + void searchBooks_WithUserRole_ShouldReturnBooks() throws Exception { + // Given + BookDto bookDto = createBookDto(); + when(bookService.search(any(BookSearchParameters.class))).thenReturn(List.of(bookDto)); + + // When + MvcResult result = mockMvc.perform(get("/books/search") + .param("titles", "Test") + .param("authors", "Author")) + .andExpect(status().isOk()) + .andReturn(); + + // Then + BookDto[] books = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto[].class); + Assertions.assertNotNull(books); + Assertions.assertEquals(1, books.length); + Assertions.assertEquals("Test Book", books[0].getTitle()); + } + + @WithMockUser(username = "admin", authorities = {"ADMIN"}) + @Test + @DisplayName("Create a new book") + void createBook_ValidRequestDto_Success() throws Exception { + // Given + CreateBookRequestDto requestDto = createBookRequestDto(); + BookDto expected = createBookDto(); + + String jsonRequest = objectMapper.writeValueAsString(requestDto); + when(bookService.save(any(CreateBookRequestDto.class))).thenReturn(expected); + + // When + MvcResult result = mockMvc.perform( + post("/books") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andReturn(); + + // Then + BookDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto.class); + Assertions.assertNotNull(actual); + Assertions.assertNotNull(actual.getId()); + Assertions.assertEquals(expected.getTitle(), actual.getTitle()); + Assertions.assertEquals(expected.getAuthor(), actual.getAuthor()); + Assertions.assertEquals(expected.getPrice(), actual.getPrice()); + } + + @WithMockUser(username = "admin", authorities = {"ADMIN"}) + @Test + @DisplayName("Delete book by ID") + void deleteBookById_WithAdminRole_ShouldDeleteBook() throws Exception { + // When & Then + mockMvc.perform(delete("/books/1")) + .andExpect(status().isNoContent()); + + verify(bookService).deleteById(1L); + } + + @WithMockUser(username = "admin", authorities = {"ADMIN"}) + @Test + @DisplayName("Update book") + void updateBook_WithAdminRole_ShouldUpdateBook() throws Exception { + // Given + CreateBookRequestDto requestDto = createBookRequestDto(); + BookDto expected = createBookDto(); + + String jsonRequest = objectMapper.writeValueAsString(requestDto); + when(bookService.update(eq(1L), any(CreateBookRequestDto.class))).thenReturn(expected); + + // When + MvcResult result = mockMvc.perform( + put("/books/1") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + + // Then + BookDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto.class); + Assertions.assertNotNull(actual); + Assertions.assertEquals(expected.getTitle(), actual.getTitle()); + Assertions.assertEquals(expected.getAuthor(), actual.getAuthor()); + Assertions.assertEquals(expected.getPrice(), actual.getPrice()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Create book with USER role should return forbidden") + void createBook_WithUserRole_ShouldReturnForbidden() throws Exception { + // Given + CreateBookRequestDto requestDto = createBookRequestDto(); + String jsonRequest = objectMapper.writeValueAsString(requestDto); + + // When & Then + mockMvc.perform( + post("/books") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isForbidden()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Delete book with USER role should return forbidden") + void deleteBook_WithUserRole_ShouldReturnForbidden() throws Exception { + // When & Then + mockMvc.perform(delete("/books/1")) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("Access without authentication should return unauthorized") + void accessWithoutAuthentication_ShouldReturnUnauthorized() throws Exception { + mockMvc.perform(get("/books")) + .andExpect(status().isUnauthorized()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get books by category ID") + void getBooksByCategoryId_WithUserRole_ShouldReturnBooks() throws Exception { + // Given + BookDtoWithoutCategoryIds bookDto = createBookDtoWithoutCategoryIds(); + when(bookService.findAllByCategoryId(1L)).thenReturn(List.of(bookDto)); + + // When + MvcResult result = mockMvc.perform(get("/categories/1/books")) + .andExpect(status().isOk()) + .andReturn(); + + // Then + BookDtoWithoutCategoryIds[] books = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDtoWithoutCategoryIds[].class + ); + Assertions.assertNotNull(books); + Assertions.assertEquals(1, books.length); + Assertions.assertEquals("Test Book", books[0].getTitle()); + } + + private BookDto createBookDto() { + BookDto dto = new BookDto(); + dto.setId(1L); + dto.setTitle("Test Book"); + dto.setAuthor("Test Author"); + dto.setIsbn("ISBN123"); + dto.setPrice(BigDecimal.valueOf(29.99)); + dto.setDescription("Test Description"); + dto.setCoverImage("cover.jpg"); + dto.setCategoryIds(Set.of(1L)); + return dto; + } + + private BookDtoWithoutCategoryIds createBookDtoWithoutCategoryIds() { + BookDtoWithoutCategoryIds dto = new BookDtoWithoutCategoryIds(); + dto.setId(1L); + dto.setTitle("Test Book"); + dto.setAuthor("Test Author"); + dto.setIsbn("ISBN123"); + dto.setPrice(BigDecimal.valueOf(29.99)); + dto.setDescription("Test Description"); + dto.setCoverImage("cover.jpg"); + return dto; + } + + private CreateBookRequestDto createBookRequestDto() { + CreateBookRequestDto dto = new CreateBookRequestDto(); + dto.setTitle("Test Book"); + dto.setAuthor("Test Author"); + dto.setIsbn("ISBN123"); + dto.setPrice(BigDecimal.valueOf(29.99)); + dto.setDescription("Test Description"); + dto.setCoverImage("cover.jpg"); + dto.setCategoryIds(Set.of(1L)); + return dto; + } +} \ No newline at end of file diff --git a/src/test/java/bookrepo/controller/CategoryControllerTest.java b/src/test/java/bookrepo/controller/CategoryControllerTest.java new file mode 100644 index 0000000..2cc5633 --- /dev/null +++ b/src/test/java/bookrepo/controller/CategoryControllerTest.java @@ -0,0 +1,185 @@ +package bookrepo.controller; + +import bookrepo.dto.category.CategoryDto; +import bookrepo.service.BookService; +import bookrepo.service.CategoryService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class CategoryControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private CategoryService categoryService; + + @MockBean + private BookService bookService; + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get all categories") + void getAll_WithUserRole_ShouldReturnCategories() throws Exception { + mockMvc.perform(get("/categories")) + .andExpect(status().isOk()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get category by ID") + void getCategoryById_WithUserRole_ShouldReturnCategory() throws Exception { + // Given + CategoryDto expected = new CategoryDto(); + expected.setId(1L); + expected.setName("Fiction"); + expected.setDescription("Fiction books"); + + when(categoryService.getById(1L)).thenReturn(expected); + + // When + MvcResult result = mockMvc.perform(get("/categories/1")) + .andExpect(status().isOk()) + .andReturn(); + + // Then + CategoryDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), CategoryDto.class); + Assertions.assertNotNull(actual); + Assertions.assertEquals(expected.getId(), actual.getId()); + Assertions.assertEquals(expected.getName(), actual.getName()); + Assertions.assertEquals(expected.getDescription(), actual.getDescription()); + } + + @WithMockUser(username = "admin", authorities = {"ADMIN"}) + @Test + @DisplayName("Create a new category") + void createCategory_ValidRequestDto_Success() throws Exception { + // Given + CategoryDto requestDto = new CategoryDto(); + requestDto.setName("Science"); + requestDto.setDescription("Science books"); + + CategoryDto expected = new CategoryDto(); + expected.setId(1L); + expected.setName("Science"); + expected.setDescription("Science books"); + + String jsonRequest = objectMapper.writeValueAsString(requestDto); + when(categoryService.save(any(CategoryDto.class))).thenReturn(expected); + + // When + MvcResult result = mockMvc.perform( + post("/categories") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andReturn(); + + // Then + CategoryDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), CategoryDto.class); + Assertions.assertNotNull(actual); + Assertions.assertNotNull(actual.getId()); + Assertions.assertEquals(expected.getName(), actual.getName()); + Assertions.assertEquals(expected.getDescription(), actual.getDescription()); + } + + @WithMockUser(username = "admin", authorities = {"ADMIN"}) + @Test + @DisplayName("Update category") + void updateCategory_ValidRequestDto_Success() throws Exception { + // Given + CategoryDto requestDto = new CategoryDto(); + requestDto.setName("Updated Fiction"); + requestDto.setDescription("Updated description"); + + CategoryDto expected = new CategoryDto(); + expected.setId(1L); + expected.setName("Updated Fiction"); + expected.setDescription("Updated description"); + + String jsonRequest = objectMapper.writeValueAsString(requestDto); + when(categoryService.update(eq(1L), any(CategoryDto.class))).thenReturn(expected); + + // When + MvcResult result = mockMvc.perform( + put("/categories/1") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + + // Then + CategoryDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), CategoryDto.class); + Assertions.assertNotNull(actual); + Assertions.assertEquals(expected.getName(), actual.getName()); + Assertions.assertEquals(expected.getDescription(), actual.getDescription()); + } + + @WithMockUser(username = "admin", authorities = {"ADMIN"}) + @Test + @DisplayName("Delete category") + void deleteCategory_WithAdminRole_ShouldDelete() throws Exception { + mockMvc.perform(delete("/categories/1")) + .andExpect(status().isNoContent()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get books by category ID") + void getBooksByCategoryId_WithUserRole_ShouldReturnBooks() throws Exception { + mockMvc.perform(get("/categories/1/books")) + .andExpect(status().isOk()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Create category with USER role should return forbidden") + void createCategory_WithUserRole_ShouldReturnForbidden() throws Exception { + CategoryDto requestDto = new CategoryDto(); + requestDto.setName("Test"); + requestDto.setDescription("Test category"); + + String jsonRequest = objectMapper.writeValueAsString(requestDto); + + mockMvc.perform( + post("/categories") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("Access without authentication should return unauthorized") + void accessWithoutAuthentication_ShouldReturnUnauthorized() throws Exception { + mockMvc.perform(get("/categories")) + .andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/src/test/java/bookrepo/repository/BookRepositoryTest.java b/src/test/java/bookrepo/repository/BookRepositoryTest.java new file mode 100644 index 0000000..82e9508 --- /dev/null +++ b/src/test/java/bookrepo/repository/BookRepositoryTest.java @@ -0,0 +1,55 @@ +package bookrepo.repository; + +import bookrepo.model.Book; +import bookrepo.model.Category; +import bookrepo.repository.book.BookRepository; +import bookrepo.repository.category.CategoryRepository; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +@Testcontainers +public class BookRepositoryTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + CategoryRepository categoryRepository; + + @Test + @DisplayName("Find all books by category ID") + void findAllByCategories_Id_finds1BookWithId1_ShouldReturnListWith1Book() { + Category category = new Category(); + category.setName("Programming"); + Category savedCategory = categoryRepository.save(category); + + Book book = new Book(); + book.setTitle("Book Title 1"); + book.setAuthor("Author 1"); + book.setDescription("Description 1"); + book.setIsbn("ISBN001"); + book.setPrice(BigDecimal.valueOf(29.99)); + book.setCategories(Set.of(savedCategory)); + + Book savedBook = bookRepository.save(book); + + List actual = bookRepository.findAllByCategories_Id(savedCategory.getId()); + + Assertions.assertEquals(1, actual.size()); + Assertions.assertEquals(savedBook.getId(), actual.get(0).getId()); + } +} \ No newline at end of file diff --git a/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java b/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java new file mode 100644 index 0000000..db3713d --- /dev/null +++ b/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java @@ -0,0 +1,112 @@ +package bookrepo.repository; + +import bookrepo.dto.book.BookSearchParameters; +import bookrepo.model.Book; +import bookrepo.repository.book.BookSpecificationBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.jpa.domain.Specification; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class BookSpecificationBuilderTest { + + @Mock + private SpecificationProviderManager bookSpecificationProviderManager; + + @Mock + private SpecificationProvider specificationProvider; + + @InjectMocks + private BookSpecificationBuilder bookSpecificationBuilder; + + @Test + void build_WithAuthorParameters_ShouldAddAuthorSpecification() { + // Given + BookSearchParameters searchParameters = new BookSearchParameters( + new String[]{"Author1", "Author2"}, null, null, null, null, null + ); + + when(bookSpecificationProviderManager.getSpecificationProvider("author")) + .thenReturn(specificationProvider); + when(specificationProvider.getSpecification(any())) + .thenReturn((root, query, cb) -> cb.conjunction()); + + // When + Specification result = bookSpecificationBuilder.build(searchParameters); + + // Then + assertNotNull(result); + verify(bookSpecificationProviderManager).getSpecificationProvider("author"); + verify(specificationProvider).getSpecification(new String[]{"Author1", "Author2"}); + } + + @Test + void build_WithTitleParameters_ShouldAddTitleSpecification() { + // Given + BookSearchParameters searchParameters = new BookSearchParameters( + null, new String[]{"Title1", "Title2"}, null, null, null, null + ); + + when(bookSpecificationProviderManager.getSpecificationProvider("title")) + .thenReturn(specificationProvider); + when(specificationProvider.getSpecification(any())) + .thenReturn((root, query, cb) -> cb.conjunction()); + + // When + Specification result = bookSpecificationBuilder.build(searchParameters); + + // Then + assertNotNull(result); + verify(bookSpecificationProviderManager).getSpecificationProvider("title"); + verify(specificationProvider).getSpecification(new String[]{"Title1", "Title2"}); + } + + @Test + void build_WithMultipleParameters_ShouldCombineSpecifications() { + // Given + BookSearchParameters searchParameters = new BookSearchParameters( + new String[]{"Author1"}, + new String[]{"Title1"}, + new String[]{"ISBN123"}, + null, null, null + ); + + when(bookSpecificationProviderManager.getSpecificationProvider("author")) + .thenReturn(specificationProvider); + when(bookSpecificationProviderManager.getSpecificationProvider("title")) + .thenReturn(specificationProvider); + when(bookSpecificationProviderManager.getSpecificationProvider("isbn")) + .thenReturn(specificationProvider); + when(specificationProvider.getSpecification(any())) + .thenReturn((root, query, cb) -> cb.conjunction()); + + // When + Specification result = bookSpecificationBuilder.build(searchParameters); + + // Then + assertNotNull(result); + verify(bookSpecificationProviderManager, times(3)).getSpecificationProvider(anyString()); + } + + @Test + void build_WithNullParameters_ShouldReturnBaseSpecification() { + // Given + BookSearchParameters searchParameters = new BookSearchParameters( + null, null, null, null, null, null + ); + + // When + Specification result = bookSpecificationBuilder.build(searchParameters); + + // Then + assertNotNull(result); + verifyNoInteractions(bookSpecificationProviderManager); + } +} \ No newline at end of file diff --git a/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java b/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java new file mode 100644 index 0000000..13032e3 --- /dev/null +++ b/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java @@ -0,0 +1,87 @@ +package bookrepo.repository; + +import bookrepo.model.Book; +import bookrepo.repository.book.BookSpecificationProviderManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class BookSpecificationProviderManagerTest { + + @Mock + private SpecificationProvider authorProvider; + + @Mock + private SpecificationProvider titleProvider; + + @InjectMocks + private BookSpecificationProviderManager bookSpecificationProviderManager; + + @Test + void getSpecificationProvider_WithExistingKey_ShouldReturnProvider() { + // Given + when(authorProvider.getKey()).thenReturn("author"); + BookSpecificationProviderManager manager = + new BookSpecificationProviderManager(List.of(authorProvider)); + + // When + SpecificationProvider result = manager.getSpecificationProvider("author"); + + // Then + assertNotNull(result); + assertEquals(authorProvider, result); + verify(authorProvider).getKey(); + } + + @Test + void getSpecificationProvider_WithNonExistingKey_ShouldThrowException() { + // Given + when(authorProvider.getKey()).thenReturn("author"); + BookSpecificationProviderManager manager = + new BookSpecificationProviderManager(List.of(authorProvider)); + + // When & Then + RuntimeException exception = assertThrows(RuntimeException.class, + () -> manager.getSpecificationProvider("nonexistent")); + + assertEquals("No specification provider found for key: nonexistent", exception.getMessage()); + } + + @Test + void getSpecificationProvider_WithMultipleProviders_ShouldReturnCorrectOne() { + // Given + when(authorProvider.getKey()).thenReturn("author"); + when(titleProvider.getKey()).thenReturn("title"); + + BookSpecificationProviderManager manager = + new BookSpecificationProviderManager(List.of(authorProvider, titleProvider)); + + // When + SpecificationProvider result = manager.getSpecificationProvider("title"); + + // Then + assertNotNull(result); + assertEquals(titleProvider, result); + verify(authorProvider).getKey(); + verify(titleProvider).getKey(); + } + + @Test + void getSpecificationProvider_WithEmptyProviders_ShouldThrowException() { + // Given + BookSpecificationProviderManager manager = + new BookSpecificationProviderManager(List.of()); + + // When & Then + RuntimeException exception = assertThrows(RuntimeException.class, + () -> manager.getSpecificationProvider("author")); + + assertEquals("No specification provider found for key: author", exception.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/bookrepo/service/BookServiceTest.java b/src/test/java/bookrepo/service/BookServiceTest.java new file mode 100644 index 0000000..6b3ef9f --- /dev/null +++ b/src/test/java/bookrepo/service/BookServiceTest.java @@ -0,0 +1,283 @@ +package bookrepo.service; + +import bookrepo.dto.book.BookDto; +import bookrepo.dto.book.BookDtoWithoutCategoryIds; +import bookrepo.dto.book.BookSearchParameters; +import bookrepo.dto.book.CreateBookRequestDto; +import bookrepo.mapper.BookMapper; +import bookrepo.model.Book; +import bookrepo.model.Category; +import bookrepo.repository.book.BookRepository; +import bookrepo.repository.book.BookSpecificationBuilder; +import bookrepo.repository.category.CategoryRepository; +import bookrepo.service.impl.BookServiceImpl; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + +@ExtendWith(MockitoExtension.class) +public class BookServiceTest { + + @Mock + private BookRepository bookRepository; + + @Mock + private CategoryRepository categoryRepository; + + @Mock + private BookMapper bookMapper; + + @Mock + private BookSpecificationBuilder bookSpecificationBuilder; + + + @InjectMocks + private BookServiceImpl bookService; + + @Test + @DisplayName(""" + Should save a book when save is called with valid request DTO + and return the corresponding BookDto with correct category IDs + """) + void save_withValidRequest_returnsBookDtoWithCategoryIds() { + CreateBookRequestDto requestDto = new CreateBookRequestDto(); + requestDto.setTitle("Effective Java"); + requestDto.setAuthor("Joshua Bloch"); + requestDto.setDescription("A comprehensive guide to best practices in Java programming."); + requestDto.setPrice(BigDecimal.valueOf(45.00)); + requestDto.setIsbn("978-0134686097"); + requestDto.setCategoryIds(Set.of(1L, 2L)); + + Book book = new Book(); + book.setTitle(requestDto.getTitle()); + book.setAuthor(requestDto.getAuthor()); + book.setDescription(requestDto.getDescription()); + book.setPrice(requestDto.getPrice()); + book.setIsbn(requestDto.getIsbn()); + + Category category1 = new Category(); + category1.setId(1L); + category1.setName("Programming"); + + Category category2 = new Category(); + category2.setId(2L); + category2.setName("Java"); + + Set categories = Set.of(category1, category2); + book.setCategories(categories); + + BookDto expectedDto = new BookDto(); + expectedDto.setTitle(book.getTitle()); + expectedDto.setAuthor(book.getAuthor()); + expectedDto.setDescription(book.getDescription()); + expectedDto.setPrice(book.getPrice()); + expectedDto.setIsbn(book.getIsbn()); + expectedDto.setCategoryIds(Set.of(1L, 2L)); + + Mockito.when(bookMapper.toModel(requestDto)).thenReturn(book); + Mockito.when(categoryRepository.findAllById(requestDto.getCategoryIds())) + .thenReturn(new ArrayList<>(categories)); + Mockito.when(bookRepository.save(book)).thenReturn(book); + Mockito.when(bookMapper.toDto(book)).thenReturn(expectedDto); + + BookDto actualDto = bookService.save(requestDto); + Assertions.assertEquals(expectedDto, actualDto); + } + + @Test + @DisplayName(""" + Should show all books when findAll is called with valid pageable + and return the corresponding Page of BookDto + """) + void findAll_withValidPageable_returnsPageOfBookDto() { + Book book1 = new Book(); + book1.setId(1L); + book1.setTitle("Effective Java"); + book1.setAuthor("Joshua Bloch"); + book1.setDescription("A comprehensive guide to best practices in Java programming."); + book1.setPrice(BigDecimal.valueOf(45.00)); + book1.setIsbn("978-0134686097"); + + Book book2 = new Book(); + book2.setId(2L); + book2.setTitle("Clean Code"); + book2.setAuthor("Robert C. Martin"); + book2.setDescription("A Handbook of Agile Software Craftsmanship."); + book2.setPrice(BigDecimal.valueOf(40.00)); + book2.setIsbn("978-0132350884"); + + Pageable pageable = PageRequest.of(0, 10); + List books = List.of(book1, book2); + Page bookPage = new PageImpl<>(books, pageable, books.size()); + + Mockito.when(bookRepository.findAll(pageable)).thenReturn(bookPage); + Mockito.when(bookMapper.toDto(book1)).thenReturn(new BookDto() {{ + setId(book1.getId()); + setTitle(book1.getTitle()); + setAuthor(book1.getAuthor()); + setDescription(book1.getDescription()); + setPrice(book1.getPrice()); + setIsbn(book1.getIsbn()); + }}); + Mockito.when(bookMapper.toDto(book2)).thenReturn(new BookDto() {{ + setId(book2.getId()); + setTitle(book2.getTitle()); + setAuthor(book2.getAuthor()); + setDescription(book2.getDescription()); + setPrice(book2.getPrice()); + setIsbn(book2.getIsbn()); + }}); + + Page actualPage = bookService.findAll(pageable); + Assertions.assertEquals(2, actualPage.getTotalElements()); + Assertions.assertEquals(1, actualPage.getTotalPages()); + Assertions.assertEquals(2, actualPage.getContent().size()); + Assertions.assertEquals("Effective Java", actualPage.getContent().get(0).getTitle()); + Assertions.assertEquals("Clean Code", actualPage.getContent().get(1).getTitle()); + } + + @Test + @DisplayName(""" + Should return BookDto when getById is called with existing ID +""") + void getById_withValidId_returnsBookDto() { + Long id = 1L; + Book book = new Book(); + book.setId(id); + book.setTitle("Effective Java"); + book.setAuthor("Joshua Bloch"); + + BookDto expectedDto = new BookDto(); + expectedDto.setId(id); + expectedDto.setTitle(book.getTitle()); + expectedDto.setAuthor(book.getAuthor()); + + Mockito.when(bookRepository.findById(id)).thenReturn(java.util.Optional.of(book)); + Mockito.when(bookMapper.toDto(book)).thenReturn(expectedDto); + + BookDto actualDto = bookService.getById(id); + Assertions.assertEquals(expectedDto, actualDto); + } + + @Test + @DisplayName(""" + Should update book when update is called with valid ID and request DTO + and return updated BookDto +""") + void update_withValidIdAndRequest_returnsUpdatedBookDto() { + Long id = 1L; + CreateBookRequestDto requestDto = new CreateBookRequestDto(); + requestDto.setTitle("Updated Title"); + requestDto.setAuthor("Updated Author"); + requestDto.setCategoryIds(Set.of(1L)); + + Book existingBook = new Book(); + existingBook.setId(id); + existingBook.setTitle("Old Title"); + + Category category = new Category(); + category.setId(1L); + category.setName("Programming"); + + Set categories = Set.of(category); + existingBook.setCategories(categories); + + BookDto expectedDto = new BookDto(); + expectedDto.setId(id); + expectedDto.setTitle(requestDto.getTitle()); + expectedDto.setAuthor(requestDto.getAuthor()); + expectedDto.setCategoryIds(Set.of(1L)); + + Mockito.when(bookRepository.findById(id)).thenReturn(java.util.Optional.of(existingBook)); + Mockito.doAnswer(invocation -> { + existingBook.setTitle(requestDto.getTitle()); + existingBook.setAuthor(requestDto.getAuthor()); + return null; + }).when(bookMapper).updateModelFromDto(requestDto, existingBook); + Mockito.when(categoryRepository.findAllById(requestDto.getCategoryIds())) + .thenReturn(new ArrayList<>(categories)); + Mockito.when(bookRepository.save(existingBook)).thenReturn(existingBook); + Mockito.when(bookMapper.toDto(existingBook)).thenReturn(expectedDto); + + BookDto actualDto = bookService.update(id, requestDto); + Assertions.assertEquals(expectedDto, actualDto); + } + + @Test + @DisplayName(""" + Should return list of BookDto when search is called with valid parameters +""") + void search_withValidParams_returnsListOfBookDto() { + BookSearchParameters params = new BookSearchParameters( + new String[] {"Joshua Bloch"}, + new String[] {"Effective Java"}, + new String[] {"Addison-Wesley"}, + new String[] {"Programming"}, + new String[] {"cover.jpg"}, + new String[] {"45.00"} + ); + + Book book = new Book(); + book.setId(1L); + book.setTitle("Effective Java"); + + BookDto dto = new BookDto(); + dto.setId(book.getId()); + dto.setTitle(book.getTitle()); + + List books = List.of(book); + + @SuppressWarnings("unchecked") + Specification spec = Mockito.mock(Specification.class); + + Mockito.when(bookSpecificationBuilder.build(params)).thenReturn(spec); + Mockito.when(bookRepository.findAll(spec)).thenReturn(books); + Mockito.when(bookMapper.toDto(book)).thenReturn(dto); + + List result = bookService.search(params); + + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("Effective Java", result.get(0).getTitle()); + } + + @Test + @DisplayName(""" + Should return list of BookDtoWithoutCategoryIds when findAllByCategoryId is called with valid ID +""") + void findAllByCategoryId_withValidId_returnsListOfBookDtoWithoutCategoryIds() { + Long categoryId = 1L; + Category category = new Category(); + category.setId(categoryId); + category.setName("Programming"); + + Book book = new Book(); + book.setId(1L); + book.setTitle("Effective Java"); + + BookDtoWithoutCategoryIds dto = new BookDtoWithoutCategoryIds(); + dto.setId(book.getId()); + dto.setTitle(book.getTitle()); + + Mockito.when(categoryRepository.findById(categoryId)).thenReturn(java.util.Optional.of(category)); + Mockito.when(bookRepository.findAllByCategories_Id(categoryId)).thenReturn(List.of(book)); + Mockito.when(bookMapper.toDtoWithoutCategories(book)).thenReturn(dto); + + List result = bookService.findAllByCategoryId(categoryId); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("Effective Java", result.get(0).getTitle()); + } +} diff --git a/src/test/java/bookrepo/service/CategoryServiceTest.java b/src/test/java/bookrepo/service/CategoryServiceTest.java new file mode 100644 index 0000000..dc9626b --- /dev/null +++ b/src/test/java/bookrepo/service/CategoryServiceTest.java @@ -0,0 +1,152 @@ +package bookrepo.service; + +import bookrepo.dto.category.CategoryDto; +import bookrepo.exception.EntityNotFoundException; +import bookrepo.mapper.CategoryMapper; +import bookrepo.model.Category; +import bookrepo.repository.category.CategoryRepository; +import bookrepo.service.impl.CategoryServiceImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +public class CategoryServiceTest { + @Mock + private CategoryRepository categoryRepository; + + @Mock + private CategoryMapper categoryMapper; + + @InjectMocks + private CategoryServiceImpl categoryService; + + @Test + @DisplayName("Should return page of CategoryDto when findAll is called with valid pageable") + void findAll_withValidPageable_returnsPageOfCategoryDto() { + Category category1 = new Category(); + category1.setId(1L); + category1.setName("Programming"); + + Category category2 = new Category(); + category2.setId(2L); + category2.setName("Java"); + + CategoryDto dto1 = new CategoryDto(); + dto1.setId(category1.getId()); + dto1.setName(category1.getName()); + + CategoryDto dto2 = new CategoryDto(); + dto2.setId(category2.getId()); + dto2.setName(category2.getName()); + + Pageable pageable = PageRequest.of(0, 10); + List categories = List.of(category1, category2); + Page categoryPage = new PageImpl<>(categories, pageable, categories.size()); + + Mockito.when(categoryRepository.findAll(pageable)).thenReturn(categoryPage); + Mockito.when(categoryMapper.toDto(category1)).thenReturn(dto1); + Mockito.when(categoryMapper.toDto(category2)).thenReturn(dto2); + + Page result = categoryService.findAll(pageable); + + Assertions.assertEquals(2, result.getTotalElements()); + Assertions.assertEquals("Programming", result.getContent().get(0).getName()); + Assertions.assertEquals("Java", result.getContent().get(1).getName()); + } + + @Test + @DisplayName("Should return CategoryDto when getById is called with existing ID") + void getById_withValidId_returnsCategoryDto() { + Long id = 1L; + Category category = new Category(); + category.setId(id); + category.setName("Programming"); + + CategoryDto dto = new CategoryDto(); + dto.setId(id); + dto.setName("Programming"); + + Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.of(category)); + Mockito.when(categoryMapper.toDto(category)).thenReturn(dto); + + CategoryDto result = categoryService.getById(id); + + Assertions.assertEquals("Programming", result.getName()); + } + + @Test + @DisplayName("Should throw EntityNotFoundException when getById is called with non-existing ID") + void getById_withInvalidId_throwsException() { + Long id = 999L; + Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.empty()); + + Assertions.assertThrows(EntityNotFoundException.class, () -> categoryService.getById(id)); + } + + @Test + @DisplayName("Should save category and return CategoryDto when save is called with valid DTO") + void save_withValidDto_returnsSavedCategoryDto() { + CategoryDto dto = new CategoryDto(); + dto.setName("Programming"); + + Category category = new Category(); + category.setName("Programming"); + + Mockito.when(categoryMapper.toEntity(dto)).thenReturn(category); + Mockito.when(categoryRepository.save(category)).thenReturn(category); + Mockito.when(categoryMapper.toDto(category)).thenReturn(dto); + + CategoryDto result = categoryService.save(dto); + + Assertions.assertEquals("Programming", result.getName()); + } + + @Test + @DisplayName("Should update category and return updated CategoryDto when update is called with valid ID and DTO") + void update_withValidIdAndDto_returnsUpdatedCategoryDto() { + Long id = 1L; + CategoryDto dto = new CategoryDto(); + dto.setName("Updated"); + + Category category = new Category(); + category.setId(id); + category.setName("Old"); + + Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.of(category)); + Mockito.doAnswer(invocation -> { + category.setName(dto.getName()); + return null; + }).when(categoryMapper).updateEntityFromDto(dto, category); + Mockito.when(categoryRepository.save(category)).thenReturn(category); + Mockito.when(categoryMapper.toDto(category)).thenReturn(dto); + + CategoryDto result = categoryService.update(id, dto); + + Assertions.assertEquals("Updated", result.getName()); + } + + @Test + @DisplayName("Should throw EntityNotFoundException when update is called with non-existing ID") + void update_withInvalidId_throwsException() { + Long id = 999L; + CategoryDto dto = new CategoryDto(); + dto.setName("New"); + + Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.empty()); + + Assertions.assertThrows(EntityNotFoundException.class, () -> categoryService.update(id, dto)); + } +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index f33f2ae..db1affc 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -1,9 +1,6 @@ -spring.datasource.url=jdbc:h2:mem:testdb -spring.datasource.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:tc:mysql:8.0.37:///mydb spring.datasource.username=sa spring.datasource.password=password -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=create-drop jwt.expiration=300000 jwt.secret=MyJwtSecretKeyj98ty4j98hgj95j98hgj98hj From 8723d0fd09a5d78f7f5b56baa8dfc87e031679e3 Mon Sep 17 00:00:00 2001 From: Egor Date: Sat, 13 Sep 2025 02:54:25 +0200 Subject: [PATCH 2/4] fixed-checkstyle --- .../controller/BookControllerTest.java | 43 +++++---- .../controller/CategoryControllerTest.java | 31 ++++--- .../repository/BookRepositoryTest.java | 6 +- .../BookSpecificationBuilderTest.java | 14 ++- .../BookSpecificationProviderManagerTest.java | 15 ++- .../bookrepo/service/BookServiceTest.java | 93 ++++++++++--------- .../bookrepo/service/CategoryServiceTest.java | 18 ++-- 7 files changed, 129 insertions(+), 91 deletions(-) diff --git a/src/test/java/bookrepo/controller/BookControllerTest.java b/src/test/java/bookrepo/controller/BookControllerTest.java index b872de9..935af36 100644 --- a/src/test/java/bookrepo/controller/BookControllerTest.java +++ b/src/test/java/bookrepo/controller/BookControllerTest.java @@ -1,11 +1,24 @@ package bookrepo.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import bookrepo.dto.book.BookDto; import bookrepo.dto.book.BookDtoWithoutCategoryIds; import bookrepo.dto.book.BookSearchParameters; import bookrepo.dto.book.CreateBookRequestDto; import bookrepo.service.BookService; import com.fasterxml.jackson.databind.ObjectMapper; +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -21,18 +34,6 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import java.math.BigDecimal; -import java.util.List; -import java.util.Set; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @@ -88,7 +89,9 @@ void getBookById_WithUserRole_ShouldReturnBook() throws Exception { .andReturn(); // Then - BookDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto.class); + BookDto actual = objectMapper.readValue(result + .getResponse() + .getContentAsString(), BookDto.class); Assertions.assertNotNull(actual); Assertions.assertEquals(expected.getId(), actual.getId()); Assertions.assertEquals(expected.getTitle(), actual.getTitle()); @@ -112,7 +115,9 @@ void searchBooks_WithUserRole_ShouldReturnBooks() throws Exception { .andReturn(); // Then - BookDto[] books = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto[].class); + BookDto[] books = objectMapper.readValue(result + .getResponse() + .getContentAsString(), BookDto[].class); Assertions.assertNotNull(books); Assertions.assertEquals(1, books.length); Assertions.assertEquals("Test Book", books[0].getTitle()); @@ -139,7 +144,9 @@ void createBook_ValidRequestDto_Success() throws Exception { .andReturn(); // Then - BookDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto.class); + BookDto actual = objectMapper.readValue(result + .getResponse() + .getContentAsString(), BookDto.class); Assertions.assertNotNull(actual); Assertions.assertNotNull(actual.getId()); Assertions.assertEquals(expected.getTitle(), actual.getTitle()); @@ -179,7 +186,9 @@ void updateBook_WithAdminRole_ShouldUpdateBook() throws Exception { .andReturn(); // Then - BookDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto.class); + BookDto actual = objectMapper.readValue(result + .getResponse() + .getContentAsString(), BookDto.class); Assertions.assertNotNull(actual); Assertions.assertEquals(expected.getTitle(), actual.getTitle()); Assertions.assertEquals(expected.getAuthor(), actual.getAuthor()); @@ -278,4 +287,4 @@ private CreateBookRequestDto createBookRequestDto() { dto.setCategoryIds(Set.of(1L)); return dto; } -} \ No newline at end of file +} diff --git a/src/test/java/bookrepo/controller/CategoryControllerTest.java b/src/test/java/bookrepo/controller/CategoryControllerTest.java index 2cc5633..e7a16f3 100644 --- a/src/test/java/bookrepo/controller/CategoryControllerTest.java +++ b/src/test/java/bookrepo/controller/CategoryControllerTest.java @@ -1,5 +1,14 @@ package bookrepo.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import bookrepo.dto.category.CategoryDto; import bookrepo.service.BookService; import bookrepo.service.CategoryService; @@ -16,14 +25,6 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @@ -68,7 +69,9 @@ void getCategoryById_WithUserRole_ShouldReturnCategory() throws Exception { .andReturn(); // Then - CategoryDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), CategoryDto.class); + CategoryDto actual = objectMapper.readValue(result + .getResponse() + .getContentAsString(), CategoryDto.class); Assertions.assertNotNull(actual); Assertions.assertEquals(expected.getId(), actual.getId()); Assertions.assertEquals(expected.getName(), actual.getName()); @@ -102,7 +105,9 @@ void createCategory_ValidRequestDto_Success() throws Exception { .andReturn(); // Then - CategoryDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), CategoryDto.class); + CategoryDto actual = objectMapper.readValue(result + .getResponse() + .getContentAsString(), CategoryDto.class); Assertions.assertNotNull(actual); Assertions.assertNotNull(actual.getId()); Assertions.assertEquals(expected.getName(), actual.getName()); @@ -136,7 +141,9 @@ void updateCategory_ValidRequestDto_Success() throws Exception { .andReturn(); // Then - CategoryDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), CategoryDto.class); + CategoryDto actual = objectMapper.readValue(result + .getResponse() + .getContentAsString(), CategoryDto.class); Assertions.assertNotNull(actual); Assertions.assertEquals(expected.getName(), actual.getName()); Assertions.assertEquals(expected.getDescription(), actual.getDescription()); @@ -182,4 +189,4 @@ void accessWithoutAuthentication_ShouldReturnUnauthorized() throws Exception { mockMvc.perform(get("/categories")) .andExpect(status().isUnauthorized()); } -} \ No newline at end of file +} diff --git a/src/test/java/bookrepo/repository/BookRepositoryTest.java b/src/test/java/bookrepo/repository/BookRepositoryTest.java index 82e9508..f8b4e93 100644 --- a/src/test/java/bookrepo/repository/BookRepositoryTest.java +++ b/src/test/java/bookrepo/repository/BookRepositoryTest.java @@ -25,10 +25,10 @@ public class BookRepositoryTest { @Autowired - BookRepository bookRepository; + private BookRepository bookRepository; @Autowired - CategoryRepository categoryRepository; + private CategoryRepository categoryRepository; @Test @DisplayName("Find all books by category ID") @@ -52,4 +52,4 @@ void findAllByCategories_Id_finds1BookWithId1_ShouldReturnListWith1Book() { Assertions.assertEquals(1, actual.size()); Assertions.assertEquals(savedBook.getId(), actual.get(0).getId()); } -} \ No newline at end of file +} diff --git a/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java b/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java index db3713d..a067ae8 100644 --- a/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java +++ b/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java @@ -1,5 +1,13 @@ package bookrepo.repository; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + import bookrepo.dto.book.BookSearchParameters; import bookrepo.model.Book; import bookrepo.repository.book.BookSpecificationBuilder; @@ -10,10 +18,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.jpa.domain.Specification; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class BookSpecificationBuilderTest { @@ -109,4 +113,4 @@ void build_WithNullParameters_ShouldReturnBaseSpecification() { assertNotNull(result); verifyNoInteractions(bookSpecificationProviderManager); } -} \ No newline at end of file +} diff --git a/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java b/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java index 13032e3..71c17fd 100644 --- a/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java +++ b/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java @@ -1,15 +1,19 @@ package bookrepo.repository; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import bookrepo.model.Book; import bookrepo.repository.book.BookSpecificationProviderManager; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.List; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class BookSpecificationProviderManagerTest { @@ -50,7 +54,8 @@ void getSpecificationProvider_WithNonExistingKey_ShouldThrowException() { RuntimeException exception = assertThrows(RuntimeException.class, () -> manager.getSpecificationProvider("nonexistent")); - assertEquals("No specification provider found for key: nonexistent", exception.getMessage()); + assertEquals("No specification provider found for key: nonexistent", + exception.getMessage()); } @Test @@ -84,4 +89,4 @@ void getSpecificationProvider_WithEmptyProviders_ShouldThrowException() { assertEquals("No specification provider found for key: author", exception.getMessage()); } -} \ No newline at end of file +} diff --git a/src/test/java/bookrepo/service/BookServiceTest.java b/src/test/java/bookrepo/service/BookServiceTest.java index 6b3ef9f..92f971d 100644 --- a/src/test/java/bookrepo/service/BookServiceTest.java +++ b/src/test/java/bookrepo/service/BookServiceTest.java @@ -44,15 +44,14 @@ public class BookServiceTest { @Mock private BookSpecificationBuilder bookSpecificationBuilder; - @InjectMocks private BookServiceImpl bookService; @Test @DisplayName(""" - Should save a book when save is called with valid request DTO - and return the corresponding BookDto with correct category IDs - """) + Should save a book when save is called with valid request DTO + and return the corresponding BookDto with correct category IDs + """) void save_withValidRequest_returnsBookDtoWithCategoryIds() { CreateBookRequestDto requestDto = new CreateBookRequestDto(); requestDto.setTitle("Effective Java"); @@ -102,7 +101,7 @@ void save_withValidRequest_returnsBookDtoWithCategoryIds() { @DisplayName(""" Should show all books when findAll is called with valid pageable and return the corresponding Page of BookDto - """) + """) void findAll_withValidPageable_returnsPageOfBookDto() { Book book1 = new Book(); book1.setId(1L); @@ -125,22 +124,26 @@ void findAll_withValidPageable_returnsPageOfBookDto() { Page bookPage = new PageImpl<>(books, pageable, books.size()); Mockito.when(bookRepository.findAll(pageable)).thenReturn(bookPage); - Mockito.when(bookMapper.toDto(book1)).thenReturn(new BookDto() {{ - setId(book1.getId()); - setTitle(book1.getTitle()); - setAuthor(book1.getAuthor()); - setDescription(book1.getDescription()); - setPrice(book1.getPrice()); - setIsbn(book1.getIsbn()); - }}); - Mockito.when(bookMapper.toDto(book2)).thenReturn(new BookDto() {{ - setId(book2.getId()); - setTitle(book2.getTitle()); - setAuthor(book2.getAuthor()); - setDescription(book2.getDescription()); - setPrice(book2.getPrice()); - setIsbn(book2.getIsbn()); - }}); + Mockito.when(bookMapper.toDto(book1)).thenReturn(new BookDto() { + { + setId(book1.getId()); + setTitle(book1.getTitle()); + setAuthor(book1.getAuthor()); + setDescription(book1.getDescription()); + setPrice(book1.getPrice()); + setIsbn(book1.getIsbn()); + } + }); + Mockito.when(bookMapper.toDto(book2)).thenReturn(new BookDto() { + { + setId(book2.getId()); + setTitle(book2.getTitle()); + setAuthor(book2.getAuthor()); + setDescription(book2.getDescription()); + setPrice(book2.getPrice()); + setIsbn(book2.getIsbn()); + } + }); Page actualPage = bookService.findAll(pageable); Assertions.assertEquals(2, actualPage.getTotalElements()); @@ -152,8 +155,8 @@ void findAll_withValidPageable_returnsPageOfBookDto() { @Test @DisplayName(""" - Should return BookDto when getById is called with existing ID -""") + Should return BookDto when getById is called with existing ID + """) void getById_withValidId_returnsBookDto() { Long id = 1L; Book book = new Book(); @@ -175,16 +178,16 @@ void getById_withValidId_returnsBookDto() { @Test @DisplayName(""" - Should update book when update is called with valid ID and request DTO - and return updated BookDto -""") + Should update book when update is called with valid ID and request DTO + and return updated BookDto + """) void update_withValidIdAndRequest_returnsUpdatedBookDto() { - Long id = 1L; CreateBookRequestDto requestDto = new CreateBookRequestDto(); requestDto.setTitle("Updated Title"); requestDto.setAuthor("Updated Author"); requestDto.setCategoryIds(Set.of(1L)); + Long id = 1L; Book existingBook = new Book(); existingBook.setId(id); existingBook.setTitle("Old Title"); @@ -219,18 +222,9 @@ void update_withValidIdAndRequest_returnsUpdatedBookDto() { @Test @DisplayName(""" - Should return list of BookDto when search is called with valid parameters -""") + Should return list of BookDto when search is called with valid parameters + """) void search_withValidParams_returnsListOfBookDto() { - BookSearchParameters params = new BookSearchParameters( - new String[] {"Joshua Bloch"}, - new String[] {"Effective Java"}, - new String[] {"Addison-Wesley"}, - new String[] {"Programming"}, - new String[] {"cover.jpg"}, - new String[] {"45.00"} - ); - Book book = new Book(); book.setId(1L); book.setTitle("Effective Java"); @@ -244,6 +238,15 @@ void search_withValidParams_returnsListOfBookDto() { @SuppressWarnings("unchecked") Specification spec = Mockito.mock(Specification.class); + BookSearchParameters params = new BookSearchParameters( + new String[]{"Joshua Bloch"}, + new String[]{"Effective Java"}, + new String[]{"Addison-Wesley"}, + new String[]{"Programming"}, + new String[]{"cover.jpg"}, + new String[]{"45.00"} + ); + Mockito.when(bookSpecificationBuilder.build(params)).thenReturn(spec); Mockito.when(bookRepository.findAll(spec)).thenReturn(books); Mockito.when(bookMapper.toDto(book)).thenReturn(dto); @@ -256,8 +259,9 @@ void search_withValidParams_returnsListOfBookDto() { @Test @DisplayName(""" - Should return list of BookDtoWithoutCategoryIds when findAllByCategoryId is called with valid ID -""") + Should return list of BookDtoWithoutCategoryIds\s + when findAllByCategoryId is called with valid ID + \s""") void findAllByCategoryId_withValidId_returnsListOfBookDtoWithoutCategoryIds() { Long categoryId = 1L; Category category = new Category(); @@ -272,9 +276,12 @@ void findAllByCategoryId_withValidId_returnsListOfBookDtoWithoutCategoryIds() { dto.setId(book.getId()); dto.setTitle(book.getTitle()); - Mockito.when(categoryRepository.findById(categoryId)).thenReturn(java.util.Optional.of(category)); - Mockito.when(bookRepository.findAllByCategories_Id(categoryId)).thenReturn(List.of(book)); - Mockito.when(bookMapper.toDtoWithoutCategories(book)).thenReturn(dto); + Mockito.when(categoryRepository.findById(categoryId)) + .thenReturn(java.util.Optional.of(category)); + Mockito.when(bookRepository.findAllByCategories_Id(categoryId)) + .thenReturn(List.of(book)); + Mockito.when(bookMapper.toDtoWithoutCategories(book)) + .thenReturn(dto); List result = bookService.findAllByCategoryId(categoryId); Assertions.assertEquals(1, result.size()); diff --git a/src/test/java/bookrepo/service/CategoryServiceTest.java b/src/test/java/bookrepo/service/CategoryServiceTest.java index dc9626b..fb903d3 100644 --- a/src/test/java/bookrepo/service/CategoryServiceTest.java +++ b/src/test/java/bookrepo/service/CategoryServiceTest.java @@ -6,6 +6,8 @@ import bookrepo.model.Category; import bookrepo.repository.category.CategoryRepository; import bookrepo.service.impl.CategoryServiceImpl; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -19,9 +21,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import java.util.List; -import java.util.Optional; - @ExtendWith(MockitoExtension.class) public class CategoryServiceTest { @Mock @@ -115,7 +114,10 @@ void save_withValidDto_returnsSavedCategoryDto() { } @Test - @DisplayName("Should update category and return updated CategoryDto when update is called with valid ID and DTO") + @DisplayName(""" + Should update category and return updated\s + CategoryDto when update is called with valid ID and DTO + \s""") void update_withValidIdAndDto_returnsUpdatedCategoryDto() { Long id = 1L; CategoryDto dto = new CategoryDto(); @@ -139,7 +141,10 @@ void update_withValidIdAndDto_returnsUpdatedCategoryDto() { } @Test - @DisplayName("Should throw EntityNotFoundException when update is called with non-existing ID") + @DisplayName(""" + Should throw EntityNotFoundException\s + when update is called with non-existing ID + \s""") void update_withInvalidId_throwsException() { Long id = 999L; CategoryDto dto = new CategoryDto(); @@ -147,6 +152,7 @@ void update_withInvalidId_throwsException() { Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.empty()); - Assertions.assertThrows(EntityNotFoundException.class, () -> categoryService.update(id, dto)); + Assertions.assertThrows(EntityNotFoundException.class, () -> + categoryService.update(id, dto)); } } From d1a1027642d38377f205e03adf1f5e684c0b7567 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 15 Sep 2025 16:07:18 +0200 Subject: [PATCH 3/4] fixed --- .../controller/BookControllerTest.java | 211 +++++++--------- .../bookrepo/service/BookServiceTest.java | 239 ++++++++---------- .../bookrepo/service/CategoryServiceTest.java | 152 ++++++----- src/test/java/bookrepo/util/TestUtil.java | 173 +++++++++++++ 4 files changed, 457 insertions(+), 318 deletions(-) create mode 100644 src/test/java/bookrepo/util/TestUtil.java diff --git a/src/test/java/bookrepo/controller/BookControllerTest.java b/src/test/java/bookrepo/controller/BookControllerTest.java index 935af36..120ab07 100644 --- a/src/test/java/bookrepo/controller/BookControllerTest.java +++ b/src/test/java/bookrepo/controller/BookControllerTest.java @@ -1,9 +1,14 @@ package bookrepo.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static bookrepo.util.TestUtil.createBookEffectiveJava; +import static bookrepo.util.TestUtil.createBookRequestDto; +import static bookrepo.util.TestUtil.createExpectedBookDto; +import static bookrepo.util.TestUtil.createJavaCategory; +import static bookrepo.util.TestUtil.createProgrammingCategory; +import static bookrepo.util.TestUtil.createValidBookRequestDto; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -12,23 +17,22 @@ import bookrepo.dto.book.BookDto; import bookrepo.dto.book.BookDtoWithoutCategoryIds; -import bookrepo.dto.book.BookSearchParameters; import bookrepo.dto.book.CreateBookRequestDto; -import bookrepo.service.BookService; +import bookrepo.model.Book; +import bookrepo.model.Category; +import bookrepo.repository.book.BookRepository; +import bookrepo.repository.category.CategoryRepository; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import java.math.BigDecimal; +import jakarta.transaction.Transactional; import java.util.List; import java.util.Set; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; @@ -38,6 +42,7 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") +@Transactional class BookControllerTest { @Autowired @@ -46,17 +51,24 @@ class BookControllerTest { @Autowired private ObjectMapper objectMapper; - @MockBean - private BookService bookService; + @Autowired + private BookRepository bookRepository; + + @Autowired + private CategoryRepository categoryRepository; @WithMockUser(username = "user", authorities = {"USER"}) @Test @DisplayName("Get all books") void findAll_WithUserRole_ShouldReturnPageOfBooks() throws Exception { // Given - BookDto bookDto = createBookDto(); - Page page = new PageImpl<>(List.of(bookDto)); - when(bookService.findAll(any(Pageable.class))).thenReturn(page); + Category categoryProgramming = categoryRepository.save(createProgrammingCategory()); + Category categoryJava = categoryRepository.save(createJavaCategory()); + Book book = createBookEffectiveJava(); + book.setCategories(Set.of(categoryProgramming, categoryJava)); + bookRepository.save(book); + + BookDto expectedDto = createExpectedBookDto(book); // When MvcResult result = mockMvc.perform(get("/books") @@ -65,14 +77,18 @@ void findAll_WithUserRole_ShouldReturnPageOfBooks() throws Exception { .andExpect(status().isOk()) .andReturn(); + // Then String responseContent = result.getResponse().getContentAsString(); - Assertions.assertTrue(responseContent.contains("Test Book")); - Assertions.assertTrue(responseContent.contains("Test Author")); - Assertions.assertTrue(responseContent.contains("29.99")); + JsonNode root = objectMapper.readTree(responseContent); + List books = objectMapper.readValue( + root.get("content").toString(), + new TypeReference>() { + } + ); - Assertions.assertTrue(responseContent.contains("\"title\":\"Test Book\"")); - Assertions.assertTrue(responseContent.contains("\"author\":\"Test Author\"")); + assertThat(books).hasSize(1); + assertThat(books.get(0).getTitle()).isEqualTo("Effective Java"); } @WithMockUser(username = "user", authorities = {"USER"}) @@ -80,23 +96,24 @@ void findAll_WithUserRole_ShouldReturnPageOfBooks() throws Exception { @DisplayName("Get book by ID") void getBookById_WithUserRole_ShouldReturnBook() throws Exception { // Given - BookDto expected = createBookDto(); - when(bookService.getById(1L)).thenReturn(expected); + Category category = categoryRepository.save(createProgrammingCategory()); + Book book = createBookEffectiveJava(); + book.setCategories(Set.of(category)); + Book savedBook = bookRepository.save(book); // When - MvcResult result = mockMvc.perform(get("/books/1")) + MvcResult result = mockMvc.perform(get("/books/{id}", savedBook.getId())) .andExpect(status().isOk()) .andReturn(); // Then - BookDto actual = objectMapper.readValue(result - .getResponse() - .getContentAsString(), BookDto.class); - Assertions.assertNotNull(actual); - Assertions.assertEquals(expected.getId(), actual.getId()); - Assertions.assertEquals(expected.getTitle(), actual.getTitle()); - Assertions.assertEquals(expected.getAuthor(), actual.getAuthor()); - Assertions.assertEquals(expected.getPrice(), actual.getPrice()); + BookDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDto.class + ); + assertNotNull(actual); + assertEquals(savedBook.getTitle(), actual.getTitle()); + assertEquals(savedBook.getAuthor(), actual.getAuthor()); } @WithMockUser(username = "user", authorities = {"USER"}) @@ -104,23 +121,23 @@ void getBookById_WithUserRole_ShouldReturnBook() throws Exception { @DisplayName("Search books") void searchBooks_WithUserRole_ShouldReturnBooks() throws Exception { // Given - BookDto bookDto = createBookDto(); - when(bookService.search(any(BookSearchParameters.class))).thenReturn(List.of(bookDto)); + bookRepository.save(createBookEffectiveJava()); // When MvcResult result = mockMvc.perform(get("/books/search") - .param("titles", "Test") - .param("authors", "Author")) + .param("titles", "Effective Java") + .param("authors", "Joshua Bloch")) .andExpect(status().isOk()) .andReturn(); // Then - BookDto[] books = objectMapper.readValue(result - .getResponse() - .getContentAsString(), BookDto[].class); - Assertions.assertNotNull(books); - Assertions.assertEquals(1, books.length); - Assertions.assertEquals("Test Book", books[0].getTitle()); + BookDto[] books = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDto[].class + ); + assertNotNull(books); + assertEquals(1, books.length); + assertEquals("Effective Java", books[0].getTitle()); } @WithMockUser(username = "admin", authorities = {"ADMIN"}) @@ -128,11 +145,10 @@ void searchBooks_WithUserRole_ShouldReturnBooks() throws Exception { @DisplayName("Create a new book") void createBook_ValidRequestDto_Success() throws Exception { // Given - CreateBookRequestDto requestDto = createBookRequestDto(); - BookDto expected = createBookDto(); - + Category categoryProgramming = categoryRepository.save(createProgrammingCategory()); + Category categoryJava = categoryRepository.save(createJavaCategory()); + CreateBookRequestDto requestDto = createValidBookRequestDto(); String jsonRequest = objectMapper.writeValueAsString(requestDto); - when(bookService.save(any(CreateBookRequestDto.class))).thenReturn(expected); // When MvcResult result = mockMvc.perform( @@ -144,25 +160,29 @@ void createBook_ValidRequestDto_Success() throws Exception { .andReturn(); // Then - BookDto actual = objectMapper.readValue(result - .getResponse() - .getContentAsString(), BookDto.class); - Assertions.assertNotNull(actual); - Assertions.assertNotNull(actual.getId()); - Assertions.assertEquals(expected.getTitle(), actual.getTitle()); - Assertions.assertEquals(expected.getAuthor(), actual.getAuthor()); - Assertions.assertEquals(expected.getPrice(), actual.getPrice()); + BookDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDto.class + ); + assertNotNull(actual); + assertEquals(requestDto.getTitle(), actual.getTitle()); + assertEquals(requestDto.getAuthor(), actual.getAuthor()); + assertEquals(1, bookRepository.count()); } @WithMockUser(username = "admin", authorities = {"ADMIN"}) @Test @DisplayName("Delete book by ID") void deleteBookById_WithAdminRole_ShouldDeleteBook() throws Exception { - // When & Then - mockMvc.perform(delete("/books/1")) + // Given + Book book = bookRepository.save(createBookEffectiveJava()); + + // When + mockMvc.perform(delete("/books/{id}", book.getId())) .andExpect(status().isNoContent()); - verify(bookService).deleteById(1L); + // Then + assertEquals(0, bookRepository.count()); } @WithMockUser(username = "admin", authorities = {"ADMIN"}) @@ -170,15 +190,15 @@ void deleteBookById_WithAdminRole_ShouldDeleteBook() throws Exception { @DisplayName("Update book") void updateBook_WithAdminRole_ShouldUpdateBook() throws Exception { // Given + Book book = bookRepository.save(createBookEffectiveJava()); CreateBookRequestDto requestDto = createBookRequestDto(); - BookDto expected = createBookDto(); + requestDto.setTitle("Updated Title"); String jsonRequest = objectMapper.writeValueAsString(requestDto); - when(bookService.update(eq(1L), any(CreateBookRequestDto.class))).thenReturn(expected); // When MvcResult result = mockMvc.perform( - put("/books/1") + put("/books/{id}", book.getId()) .content(jsonRequest) .contentType(MediaType.APPLICATION_JSON) ) @@ -186,24 +206,21 @@ void updateBook_WithAdminRole_ShouldUpdateBook() throws Exception { .andReturn(); // Then - BookDto actual = objectMapper.readValue(result - .getResponse() - .getContentAsString(), BookDto.class); - Assertions.assertNotNull(actual); - Assertions.assertEquals(expected.getTitle(), actual.getTitle()); - Assertions.assertEquals(expected.getAuthor(), actual.getAuthor()); - Assertions.assertEquals(expected.getPrice(), actual.getPrice()); + BookDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDto.class + ); + assertNotNull(actual); + assertEquals("Updated Title", actual.getTitle()); } @WithMockUser(username = "user", authorities = {"USER"}) @Test @DisplayName("Create book with USER role should return forbidden") void createBook_WithUserRole_ShouldReturnForbidden() throws Exception { - // Given CreateBookRequestDto requestDto = createBookRequestDto(); String jsonRequest = objectMapper.writeValueAsString(requestDto); - // When & Then mockMvc.perform( post("/books") .content(jsonRequest) @@ -216,8 +233,7 @@ void createBook_WithUserRole_ShouldReturnForbidden() throws Exception { @Test @DisplayName("Delete book with USER role should return forbidden") void deleteBook_WithUserRole_ShouldReturnForbidden() throws Exception { - // When & Then - mockMvc.perform(delete("/books/1")) + mockMvc.perform(delete("/books/{id}", 1L)) .andExpect(status().isForbidden()); } @@ -233,11 +249,13 @@ void accessWithoutAuthentication_ShouldReturnUnauthorized() throws Exception { @DisplayName("Get books by category ID") void getBooksByCategoryId_WithUserRole_ShouldReturnBooks() throws Exception { // Given - BookDtoWithoutCategoryIds bookDto = createBookDtoWithoutCategoryIds(); - when(bookService.findAllByCategoryId(1L)).thenReturn(List.of(bookDto)); + Category category = categoryRepository.save(createProgrammingCategory()); + Book book = createBookEffectiveJava(); + book.setCategories(Set.of(category)); + bookRepository.save(book); // When - MvcResult result = mockMvc.perform(get("/categories/1/books")) + MvcResult result = mockMvc.perform(get("/categories/{id}/books", category.getId())) .andExpect(status().isOk()) .andReturn(); @@ -246,45 +264,8 @@ void getBooksByCategoryId_WithUserRole_ShouldReturnBooks() throws Exception { result.getResponse().getContentAsString(), BookDtoWithoutCategoryIds[].class ); - Assertions.assertNotNull(books); - Assertions.assertEquals(1, books.length); - Assertions.assertEquals("Test Book", books[0].getTitle()); - } - - private BookDto createBookDto() { - BookDto dto = new BookDto(); - dto.setId(1L); - dto.setTitle("Test Book"); - dto.setAuthor("Test Author"); - dto.setIsbn("ISBN123"); - dto.setPrice(BigDecimal.valueOf(29.99)); - dto.setDescription("Test Description"); - dto.setCoverImage("cover.jpg"); - dto.setCategoryIds(Set.of(1L)); - return dto; - } - - private BookDtoWithoutCategoryIds createBookDtoWithoutCategoryIds() { - BookDtoWithoutCategoryIds dto = new BookDtoWithoutCategoryIds(); - dto.setId(1L); - dto.setTitle("Test Book"); - dto.setAuthor("Test Author"); - dto.setIsbn("ISBN123"); - dto.setPrice(BigDecimal.valueOf(29.99)); - dto.setDescription("Test Description"); - dto.setCoverImage("cover.jpg"); - return dto; - } - - private CreateBookRequestDto createBookRequestDto() { - CreateBookRequestDto dto = new CreateBookRequestDto(); - dto.setTitle("Test Book"); - dto.setAuthor("Test Author"); - dto.setIsbn("ISBN123"); - dto.setPrice(BigDecimal.valueOf(29.99)); - dto.setDescription("Test Description"); - dto.setCoverImage("cover.jpg"); - dto.setCategoryIds(Set.of(1L)); - return dto; + assertNotNull(books); + assertEquals(1, books.length); + assertEquals(book.getTitle(), books[0].getTitle()); } } diff --git a/src/test/java/bookrepo/service/BookServiceTest.java b/src/test/java/bookrepo/service/BookServiceTest.java index 92f971d..0174679 100644 --- a/src/test/java/bookrepo/service/BookServiceTest.java +++ b/src/test/java/bookrepo/service/BookServiceTest.java @@ -1,5 +1,9 @@ package bookrepo.service; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import bookrepo.dto.book.BookDto; import bookrepo.dto.book.BookDtoWithoutCategoryIds; import bookrepo.dto.book.BookSearchParameters; @@ -11,11 +15,10 @@ import bookrepo.repository.book.BookSpecificationBuilder; import bookrepo.repository.category.CategoryRepository; import bookrepo.service.impl.BookServiceImpl; -import java.math.BigDecimal; +import bookrepo.util.TestUtil; import java.util.ArrayList; import java.util.List; import java.util.Set; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,48 +56,32 @@ public class BookServiceTest { and return the corresponding BookDto with correct category IDs """) void save_withValidRequest_returnsBookDtoWithCategoryIds() { - CreateBookRequestDto requestDto = new CreateBookRequestDto(); - requestDto.setTitle("Effective Java"); - requestDto.setAuthor("Joshua Bloch"); - requestDto.setDescription("A comprehensive guide to best practices in Java programming."); - requestDto.setPrice(BigDecimal.valueOf(45.00)); - requestDto.setIsbn("978-0134686097"); - requestDto.setCategoryIds(Set.of(1L, 2L)); - - Book book = new Book(); - book.setTitle(requestDto.getTitle()); - book.setAuthor(requestDto.getAuthor()); - book.setDescription(requestDto.getDescription()); - book.setPrice(requestDto.getPrice()); - book.setIsbn(requestDto.getIsbn()); - - Category category1 = new Category(); - category1.setId(1L); - category1.setName("Programming"); - - Category category2 = new Category(); - category2.setId(2L); - category2.setName("Java"); - - Set categories = Set.of(category1, category2); + // Given + CreateBookRequestDto requestDto = TestUtil.createValidBookRequestDto(); + Book book = TestUtil.createBookFromRequest(requestDto); + + Category categoryProgramming = TestUtil.createProgrammingCategory(); + Category categoryJava = TestUtil.createJavaCategory(); + Set categories = Set.of(categoryProgramming, categoryJava); book.setCategories(categories); - BookDto expectedDto = new BookDto(); - expectedDto.setTitle(book.getTitle()); - expectedDto.setAuthor(book.getAuthor()); - expectedDto.setDescription(book.getDescription()); - expectedDto.setPrice(book.getPrice()); - expectedDto.setIsbn(book.getIsbn()); - expectedDto.setCategoryIds(Set.of(1L, 2L)); - - Mockito.when(bookMapper.toModel(requestDto)).thenReturn(book); - Mockito.when(categoryRepository.findAllById(requestDto.getCategoryIds())) + BookDto expectedDto = TestUtil.createExpectedBookDto(book); + + when(bookMapper.toModel(requestDto)).thenReturn(book); + when(categoryRepository.findAllById(requestDto.getCategoryIds())) .thenReturn(new ArrayList<>(categories)); - Mockito.when(bookRepository.save(book)).thenReturn(book); - Mockito.when(bookMapper.toDto(book)).thenReturn(expectedDto); + when(bookRepository.save(book)).thenReturn(book); + when(bookMapper.toDto(book)).thenReturn(expectedDto); + // When BookDto actualDto = bookService.save(requestDto); - Assertions.assertEquals(expectedDto, actualDto); + + // Then + assertEquals(expectedDto, actualDto); + verify(bookMapper).toModel(requestDto); + verify(categoryRepository).findAllById(requestDto.getCategoryIds()); + verify(bookRepository).save(book); + verify(bookMapper).toDto(book); } @Test @@ -103,54 +90,34 @@ void save_withValidRequest_returnsBookDtoWithCategoryIds() { and return the corresponding Page of BookDto """) void findAll_withValidPageable_returnsPageOfBookDto() { - Book book1 = new Book(); - book1.setId(1L); - book1.setTitle("Effective Java"); - book1.setAuthor("Joshua Bloch"); - book1.setDescription("A comprehensive guide to best practices in Java programming."); - book1.setPrice(BigDecimal.valueOf(45.00)); - book1.setIsbn("978-0134686097"); - - Book book2 = new Book(); - book2.setId(2L); - book2.setTitle("Clean Code"); - book2.setAuthor("Robert C. Martin"); - book2.setDescription("A Handbook of Agile Software Craftsmanship."); - book2.setPrice(BigDecimal.valueOf(40.00)); - book2.setIsbn("978-0132350884"); + // Given + Book book1 = TestUtil.createBookEffectiveJava(); + Book book2 = TestUtil.createBookCleanCode(); Pageable pageable = PageRequest.of(0, 10); List books = List.of(book1, book2); Page bookPage = new PageImpl<>(books, pageable, books.size()); - Mockito.when(bookRepository.findAll(pageable)).thenReturn(bookPage); - Mockito.when(bookMapper.toDto(book1)).thenReturn(new BookDto() { - { - setId(book1.getId()); - setTitle(book1.getTitle()); - setAuthor(book1.getAuthor()); - setDescription(book1.getDescription()); - setPrice(book1.getPrice()); - setIsbn(book1.getIsbn()); - } - }); - Mockito.when(bookMapper.toDto(book2)).thenReturn(new BookDto() { - { - setId(book2.getId()); - setTitle(book2.getTitle()); - setAuthor(book2.getAuthor()); - setDescription(book2.getDescription()); - setPrice(book2.getPrice()); - setIsbn(book2.getIsbn()); - } - }); + BookDto dto1 = TestUtil.createBookDtoEffectiveJava(book1); + BookDto dto2 = TestUtil.createBookDtoCleanCode(book2); + + when(bookRepository.findAll(pageable)).thenReturn(bookPage); + when(bookMapper.toDto(book1)).thenReturn(dto1); + when(bookMapper.toDto(book2)).thenReturn(dto2); + // When Page actualPage = bookService.findAll(pageable); - Assertions.assertEquals(2, actualPage.getTotalElements()); - Assertions.assertEquals(1, actualPage.getTotalPages()); - Assertions.assertEquals(2, actualPage.getContent().size()); - Assertions.assertEquals("Effective Java", actualPage.getContent().get(0).getTitle()); - Assertions.assertEquals("Clean Code", actualPage.getContent().get(1).getTitle()); + + // Then + assertEquals(2, actualPage.getTotalElements()); + assertEquals(1, actualPage.getTotalPages()); + assertEquals(2, actualPage.getContent().size()); + assertEquals("Effective Java", actualPage.getContent().get(0).getTitle()); + assertEquals("Clean Code", actualPage.getContent().get(1).getTitle()); + + verify(bookRepository).findAll(pageable); + verify(bookMapper).toDto(book1); + verify(bookMapper).toDto(book2); } @Test @@ -158,22 +125,21 @@ void findAll_withValidPageable_returnsPageOfBookDto() { Should return BookDto when getById is called with existing ID """) void getById_withValidId_returnsBookDto() { + // Given Long id = 1L; - Book book = new Book(); - book.setId(id); - book.setTitle("Effective Java"); - book.setAuthor("Joshua Bloch"); - - BookDto expectedDto = new BookDto(); - expectedDto.setId(id); - expectedDto.setTitle(book.getTitle()); - expectedDto.setAuthor(book.getAuthor()); + Book book = TestUtil.createBookEffectiveJava(); + BookDto expectedDto = TestUtil.createBookDtoEffectiveJava(book); - Mockito.when(bookRepository.findById(id)).thenReturn(java.util.Optional.of(book)); - Mockito.when(bookMapper.toDto(book)).thenReturn(expectedDto); + when(bookRepository.findById(id)).thenReturn(java.util.Optional.of(book)); + when(bookMapper.toDto(book)).thenReturn(expectedDto); + // When BookDto actualDto = bookService.getById(id); - Assertions.assertEquals(expectedDto, actualDto); + + // Then + assertEquals(expectedDto, actualDto); + verify(bookRepository).findById(id); + verify(bookMapper).toDto(book); } @Test @@ -182,20 +148,17 @@ void getById_withValidId_returnsBookDto() { and return updated BookDto """) void update_withValidIdAndRequest_returnsUpdatedBookDto() { + // Given CreateBookRequestDto requestDto = new CreateBookRequestDto(); requestDto.setTitle("Updated Title"); requestDto.setAuthor("Updated Author"); requestDto.setCategoryIds(Set.of(1L)); Long id = 1L; - Book existingBook = new Book(); - existingBook.setId(id); + Book existingBook = TestUtil.createBookEffectiveJava(); existingBook.setTitle("Old Title"); - Category category = new Category(); - category.setId(1L); - category.setName("Programming"); - + Category category = TestUtil.createProgrammingCategory(); Set categories = Set.of(category); existingBook.setCategories(categories); @@ -205,19 +168,22 @@ void update_withValidIdAndRequest_returnsUpdatedBookDto() { expectedDto.setAuthor(requestDto.getAuthor()); expectedDto.setCategoryIds(Set.of(1L)); - Mockito.when(bookRepository.findById(id)).thenReturn(java.util.Optional.of(existingBook)); - Mockito.doAnswer(invocation -> { - existingBook.setTitle(requestDto.getTitle()); - existingBook.setAuthor(requestDto.getAuthor()); - return null; - }).when(bookMapper).updateModelFromDto(requestDto, existingBook); - Mockito.when(categoryRepository.findAllById(requestDto.getCategoryIds())) + when(bookRepository.findById(id)).thenReturn(java.util.Optional.of(existingBook)); + when(categoryRepository.findAllById(requestDto.getCategoryIds())) .thenReturn(new ArrayList<>(categories)); - Mockito.when(bookRepository.save(existingBook)).thenReturn(existingBook); - Mockito.when(bookMapper.toDto(existingBook)).thenReturn(expectedDto); + when(bookRepository.save(existingBook)).thenReturn(existingBook); + when(bookMapper.toDto(existingBook)).thenReturn(expectedDto); + // When BookDto actualDto = bookService.update(id, requestDto); - Assertions.assertEquals(expectedDto, actualDto); + + // Then + assertEquals(expectedDto, actualDto); + verify(bookRepository).findById(id); + verify(bookMapper).updateModelFromDto(requestDto, existingBook); + verify(categoryRepository).findAllById(requestDto.getCategoryIds()); + verify(bookRepository).save(existingBook); + verify(bookMapper).toDto(existingBook); } @Test @@ -225,14 +191,9 @@ void update_withValidIdAndRequest_returnsUpdatedBookDto() { Should return list of BookDto when search is called with valid parameters """) void search_withValidParams_returnsListOfBookDto() { - Book book = new Book(); - book.setId(1L); - book.setTitle("Effective Java"); - - BookDto dto = new BookDto(); - dto.setId(book.getId()); - dto.setTitle(book.getTitle()); - + // Given + Book book = TestUtil.createBookEffectiveJava(); + BookDto dto = TestUtil.createBookDtoEffectiveJava(book); List books = List.of(book); @SuppressWarnings("unchecked") @@ -247,14 +208,19 @@ void search_withValidParams_returnsListOfBookDto() { new String[]{"45.00"} ); - Mockito.when(bookSpecificationBuilder.build(params)).thenReturn(spec); - Mockito.when(bookRepository.findAll(spec)).thenReturn(books); - Mockito.when(bookMapper.toDto(book)).thenReturn(dto); + when(bookSpecificationBuilder.build(params)).thenReturn(spec); + when(bookRepository.findAll(spec)).thenReturn(books); + when(bookMapper.toDto(book)).thenReturn(dto); + // When List result = bookService.search(params); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals("Effective Java", result.get(0).getTitle()); + // Then + assertEquals(1, result.size()); + assertEquals("Effective Java", result.get(0).getTitle()); + verify(bookSpecificationBuilder).build(params); + verify(bookRepository).findAll(spec); + verify(bookMapper).toDto(book); } @Test @@ -263,28 +229,27 @@ void search_withValidParams_returnsListOfBookDto() { when findAllByCategoryId is called with valid ID \s""") void findAllByCategoryId_withValidId_returnsListOfBookDtoWithoutCategoryIds() { + // Given Long categoryId = 1L; - Category category = new Category(); - category.setId(categoryId); - category.setName("Programming"); - - Book book = new Book(); - book.setId(1L); - book.setTitle("Effective Java"); + Category category = TestUtil.createProgrammingCategory(); + Book book = TestUtil.createBookEffectiveJava(); + BookDtoWithoutCategoryIds dto = TestUtil.createBookDtoWithoutCategories(book); - BookDtoWithoutCategoryIds dto = new BookDtoWithoutCategoryIds(); - dto.setId(book.getId()); - dto.setTitle(book.getTitle()); - - Mockito.when(categoryRepository.findById(categoryId)) + when(categoryRepository.findById(categoryId)) .thenReturn(java.util.Optional.of(category)); - Mockito.when(bookRepository.findAllByCategories_Id(categoryId)) + when(bookRepository.findAllByCategories_Id(categoryId)) .thenReturn(List.of(book)); - Mockito.when(bookMapper.toDtoWithoutCategories(book)) + when(bookMapper.toDtoWithoutCategories(book)) .thenReturn(dto); + // When List result = bookService.findAllByCategoryId(categoryId); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals("Effective Java", result.get(0).getTitle()); + + // Then + assertEquals(1, result.size()); + assertEquals("Effective Java", result.get(0).getTitle()); + verify(categoryRepository).findById(categoryId); + verify(bookRepository).findAllByCategories_Id(categoryId); + verify(bookMapper).toDtoWithoutCategories(book); } } diff --git a/src/test/java/bookrepo/service/CategoryServiceTest.java b/src/test/java/bookrepo/service/CategoryServiceTest.java index fb903d3..d90a0d8 100644 --- a/src/test/java/bookrepo/service/CategoryServiceTest.java +++ b/src/test/java/bookrepo/service/CategoryServiceTest.java @@ -1,5 +1,16 @@ package bookrepo.service; +import static bookrepo.util.TestUtil.createCategoryDtoWithoutId; +import static bookrepo.util.TestUtil.createJavaCategory; +import static bookrepo.util.TestUtil.createJavaCategoryDto; +import static bookrepo.util.TestUtil.createProgrammingCategory; +import static bookrepo.util.TestUtil.createProgrammingCategoryDto; +import static bookrepo.util.TestUtil.createUpdatedCategoryDto; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import bookrepo.dto.category.CategoryDto; import bookrepo.exception.EntityNotFoundException; import bookrepo.mapper.CategoryMapper; @@ -8,13 +19,11 @@ import bookrepo.service.impl.CategoryServiceImpl; import java.util.List; import java.util.Optional; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -23,6 +32,11 @@ @ExtendWith(MockitoExtension.class) public class CategoryServiceTest { + + private static final Long EXISTING_CATEGORY_ID = 1L; + private static final Long ANOTHER_CATEGORY_ID = 2L; + private static final Long NON_EXISTING_CATEGORY_ID = 999L; + @Mock private CategoryRepository categoryRepository; @@ -35,82 +49,87 @@ public class CategoryServiceTest { @Test @DisplayName("Should return page of CategoryDto when findAll is called with valid pageable") void findAll_withValidPageable_returnsPageOfCategoryDto() { - Category category1 = new Category(); - category1.setId(1L); - category1.setName("Programming"); - - Category category2 = new Category(); - category2.setId(2L); - category2.setName("Java"); - - CategoryDto dto1 = new CategoryDto(); - dto1.setId(category1.getId()); - dto1.setName(category1.getName()); + // Given + Category category1 = createProgrammingCategory(); + Category category2 = createJavaCategory(); - CategoryDto dto2 = new CategoryDto(); - dto2.setId(category2.getId()); - dto2.setName(category2.getName()); + CategoryDto dto1 = createProgrammingCategoryDto(); + CategoryDto dto2 = createJavaCategoryDto(); Pageable pageable = PageRequest.of(0, 10); List categories = List.of(category1, category2); Page categoryPage = new PageImpl<>(categories, pageable, categories.size()); - Mockito.when(categoryRepository.findAll(pageable)).thenReturn(categoryPage); - Mockito.when(categoryMapper.toDto(category1)).thenReturn(dto1); - Mockito.when(categoryMapper.toDto(category2)).thenReturn(dto2); + when(categoryRepository.findAll(pageable)).thenReturn(categoryPage); + when(categoryMapper.toDto(category1)).thenReturn(dto1); + when(categoryMapper.toDto(category2)).thenReturn(dto2); + // When Page result = categoryService.findAll(pageable); - Assertions.assertEquals(2, result.getTotalElements()); - Assertions.assertEquals("Programming", result.getContent().get(0).getName()); - Assertions.assertEquals("Java", result.getContent().get(1).getName()); + // Then + assertEquals(2, result.getTotalElements()); + assertEquals("Programming", result.getContent().get(0).getName()); + assertEquals("Java", result.getContent().get(1).getName()); + + verify(categoryRepository).findAll(pageable); + verify(categoryMapper).toDto(category1); + verify(categoryMapper).toDto(category2); } @Test @DisplayName("Should return CategoryDto when getById is called with existing ID") void getById_withValidId_returnsCategoryDto() { - Long id = 1L; - Category category = new Category(); - category.setId(id); - category.setName("Programming"); + // Given + Category category = createProgrammingCategory(); + CategoryDto dto = createProgrammingCategoryDto(); - CategoryDto dto = new CategoryDto(); - dto.setId(id); - dto.setName("Programming"); + when(categoryRepository.findById(EXISTING_CATEGORY_ID)).thenReturn(Optional.of(category)); + when(categoryMapper.toDto(category)).thenReturn(dto); - Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.of(category)); - Mockito.when(categoryMapper.toDto(category)).thenReturn(dto); + // When + CategoryDto result = categoryService.getById(EXISTING_CATEGORY_ID); - CategoryDto result = categoryService.getById(id); + // Then + assertEquals("Programming", result.getName()); - Assertions.assertEquals("Programming", result.getName()); + verify(categoryRepository).findById(EXISTING_CATEGORY_ID); + verify(categoryMapper).toDto(category); } @Test @DisplayName("Should throw EntityNotFoundException when getById is called with non-existing ID") void getById_withInvalidId_throwsException() { - Long id = 999L; - Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.empty()); + // Given + when(categoryRepository.findById(NON_EXISTING_CATEGORY_ID)).thenReturn(Optional.empty()); + + // When & Then + assertThrows(EntityNotFoundException.class, () -> + categoryService.getById(NON_EXISTING_CATEGORY_ID)); - Assertions.assertThrows(EntityNotFoundException.class, () -> categoryService.getById(id)); + verify(categoryRepository).findById(NON_EXISTING_CATEGORY_ID); } @Test @DisplayName("Should save category and return CategoryDto when save is called with valid DTO") void save_withValidDto_returnsSavedCategoryDto() { - CategoryDto dto = new CategoryDto(); - dto.setName("Programming"); - - Category category = new Category(); - category.setName("Programming"); + // Given + CategoryDto dto = createCategoryDtoWithoutId(); + Category category = createProgrammingCategory(); - Mockito.when(categoryMapper.toEntity(dto)).thenReturn(category); - Mockito.when(categoryRepository.save(category)).thenReturn(category); - Mockito.when(categoryMapper.toDto(category)).thenReturn(dto); + when(categoryMapper.toEntity(dto)).thenReturn(category); + when(categoryRepository.save(category)).thenReturn(category); + when(categoryMapper.toDto(category)).thenReturn(dto); + // When CategoryDto result = categoryService.save(dto); - Assertions.assertEquals("Programming", result.getName()); + // Then + assertEquals("Programming", result.getName()); + + verify(categoryMapper).toEntity(dto); + verify(categoryRepository).save(category); + verify(categoryMapper).toDto(category); } @Test @@ -119,25 +138,24 @@ void save_withValidDto_returnsSavedCategoryDto() { CategoryDto when update is called with valid ID and DTO \s""") void update_withValidIdAndDto_returnsUpdatedCategoryDto() { - Long id = 1L; - CategoryDto dto = new CategoryDto(); - dto.setName("Updated"); + // Given + CategoryDto dto = createUpdatedCategoryDto(); + Category category = createProgrammingCategory(); - Category category = new Category(); - category.setId(id); - category.setName("Old"); + when(categoryRepository.findById(EXISTING_CATEGORY_ID)).thenReturn(Optional.of(category)); + when(categoryRepository.save(category)).thenReturn(category); + when(categoryMapper.toDto(category)).thenReturn(dto); - Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.of(category)); - Mockito.doAnswer(invocation -> { - category.setName(dto.getName()); - return null; - }).when(categoryMapper).updateEntityFromDto(dto, category); - Mockito.when(categoryRepository.save(category)).thenReturn(category); - Mockito.when(categoryMapper.toDto(category)).thenReturn(dto); + // When + CategoryDto result = categoryService.update(EXISTING_CATEGORY_ID, dto); - CategoryDto result = categoryService.update(id, dto); + // Then + assertEquals("Updated", result.getName()); - Assertions.assertEquals("Updated", result.getName()); + verify(categoryRepository).findById(EXISTING_CATEGORY_ID); + verify(categoryMapper).updateEntityFromDto(dto, category); + verify(categoryRepository).save(category); + verify(categoryMapper).toDto(category); } @Test @@ -146,13 +164,15 @@ void update_withValidIdAndDto_returnsUpdatedCategoryDto() { when update is called with non-existing ID \s""") void update_withInvalidId_throwsException() { - Long id = 999L; - CategoryDto dto = new CategoryDto(); - dto.setName("New"); + // Given + CategoryDto dto = createUpdatedCategoryDto(); + + when(categoryRepository.findById(NON_EXISTING_CATEGORY_ID)).thenReturn(Optional.empty()); - Mockito.when(categoryRepository.findById(id)).thenReturn(Optional.empty()); + // When & Then + assertThrows(EntityNotFoundException.class, () -> + categoryService.update(NON_EXISTING_CATEGORY_ID, dto)); - Assertions.assertThrows(EntityNotFoundException.class, () -> - categoryService.update(id, dto)); + verify(categoryRepository).findById(NON_EXISTING_CATEGORY_ID); } } diff --git a/src/test/java/bookrepo/util/TestUtil.java b/src/test/java/bookrepo/util/TestUtil.java new file mode 100644 index 0000000..d61b03f --- /dev/null +++ b/src/test/java/bookrepo/util/TestUtil.java @@ -0,0 +1,173 @@ +package bookrepo.util; + +import bookrepo.dto.book.BookDto; +import bookrepo.dto.book.BookDtoWithoutCategoryIds; +import bookrepo.dto.book.CreateBookRequestDto; +import bookrepo.dto.category.CategoryDto; +import bookrepo.model.Book; +import bookrepo.model.Category; +import java.math.BigDecimal; +import java.util.Set; + +public class TestUtil { + + public static CreateBookRequestDto createValidBookRequestDto() { + CreateBookRequestDto requestDto = new CreateBookRequestDto(); + requestDto.setTitle("Effective Java"); + requestDto.setAuthor("Joshua Bloch"); + requestDto.setDescription("A comprehensive guide to best practices in Java programming."); + requestDto.setPrice(BigDecimal.valueOf(45.00)); + requestDto.setIsbn("978-0134686097"); + requestDto.setCategoryIds(Set.of(1L, 2L)); + return requestDto; + } + + public static Book createBookFromRequest(CreateBookRequestDto requestDto) { + Book book = new Book(); + book.setTitle(requestDto.getTitle()); + book.setAuthor(requestDto.getAuthor()); + book.setDescription(requestDto.getDescription()); + book.setPrice(requestDto.getPrice()); + book.setIsbn(requestDto.getIsbn()); + return book; + } + + public static Category createProgrammingCategory() { + Category category = new Category(); + category.setId(1L); + category.setName("Programming"); + return category; + } + + public static Category createJavaCategory() { + Category category = new Category(); + category.setId(2L); + category.setName("Java"); + return category; + } + + public static BookDto createExpectedBookDto(Book book) { + BookDto expectedDto = new BookDto(); + expectedDto.setTitle(book.getTitle()); + expectedDto.setAuthor(book.getAuthor()); + expectedDto.setDescription(book.getDescription()); + expectedDto.setPrice(book.getPrice()); + expectedDto.setIsbn(book.getIsbn()); + expectedDto.setCategoryIds(Set.of(1L, 2L)); + return expectedDto; + } + + public static Book createBookEffectiveJava() { + Book book = new Book(); + book.setId(1L); + book.setTitle("Effective Java"); + book.setAuthor("Joshua Bloch"); + book.setDescription("A comprehensive guide to best practices in Java programming."); + book.setPrice(BigDecimal.valueOf(45.00)); + book.setIsbn("978-0134686097"); + return book; + } + + public static Book createBookCleanCode() { + Book book = new Book(); + book.setId(2L); + book.setTitle("Clean Code"); + book.setAuthor("Robert C. Martin"); + book.setDescription("A Handbook of Agile Software Craftsmanship."); + book.setPrice(BigDecimal.valueOf(40.00)); + book.setIsbn("978-0132350884"); + return book; + } + + public static BookDto createBookDtoEffectiveJava(Book book) { + BookDto dto = new BookDto(); + dto.setId(book.getId()); + dto.setTitle(book.getTitle()); + dto.setAuthor(book.getAuthor()); + dto.setDescription(book.getDescription()); + dto.setPrice(book.getPrice()); + dto.setIsbn(book.getIsbn()); + return dto; + } + + public static BookDto createBookDtoCleanCode(Book book) { + BookDto dto = new BookDto(); + dto.setId(book.getId()); + dto.setTitle(book.getTitle()); + dto.setAuthor(book.getAuthor()); + dto.setDescription(book.getDescription()); + dto.setPrice(book.getPrice()); + dto.setIsbn(book.getIsbn()); + return dto; + } + + public static BookDtoWithoutCategoryIds createBookDtoWithoutCategories(Book book) { + BookDtoWithoutCategoryIds dto = new BookDtoWithoutCategoryIds(); + dto.setId(book.getId()); + dto.setTitle(book.getTitle()); + return dto; + } + + public static BookDto createBookDto() { + BookDto dto = new BookDto(); + dto.setId(1L); + dto.setTitle("Test Book"); + dto.setAuthor("Test Author"); + dto.setIsbn("ISBN123"); + dto.setPrice(BigDecimal.valueOf(29.99)); + dto.setDescription("Test Description"); + dto.setCoverImage("cover.jpg"); + dto.setCategoryIds(Set.of(1L)); + return dto; + } + + public static BookDtoWithoutCategoryIds createBookDtoWithoutCategoryIds() { + BookDtoWithoutCategoryIds dto = new BookDtoWithoutCategoryIds(); + dto.setId(1L); + dto.setTitle("Test Book"); + dto.setAuthor("Test Author"); + dto.setIsbn("ISBN123"); + dto.setPrice(BigDecimal.valueOf(29.99)); + dto.setDescription("Test Description"); + dto.setCoverImage("cover.jpg"); + return dto; + } + + public static CreateBookRequestDto createBookRequestDto() { + CreateBookRequestDto dto = new CreateBookRequestDto(); + dto.setTitle("Test Book"); + dto.setAuthor("Test Author"); + dto.setIsbn("ISBN123"); + dto.setPrice(BigDecimal.valueOf(29.99)); + dto.setDescription("Test Description"); + dto.setCoverImage("cover.jpg"); + dto.setCategoryIds(Set.of(1L)); + return dto; + } + + public static CategoryDto createProgrammingCategoryDto() { + CategoryDto dto = new CategoryDto(); + dto.setId(1L); + dto.setName("Programming"); + return dto; + } + + public static CategoryDto createJavaCategoryDto() { + CategoryDto dto = new CategoryDto(); + dto.setId(2L); + dto.setName("Java"); + return dto; + } + + public static CategoryDto createCategoryDtoWithoutId() { + CategoryDto dto = new CategoryDto(); + dto.setName("Programming"); + return dto; + } + + public static CategoryDto createUpdatedCategoryDto() { + CategoryDto dto = new CategoryDto(); + dto.setName("Updated"); + return dto; + } +} From 7804c704d5020a733cbd3814c57cf027c0a3e948 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 16 Sep 2025 16:45:20 +0200 Subject: [PATCH 4/4] fixed --- .../repository/book/BookRepository.java | 7 + .../bookrepo/BookRepoApplicationTests.java | 2 - .../controller/BookControllerTest.java | 284 ++++++++++++++---- .../controller/CategoryControllerTest.java | 162 +++++----- .../repository/BookRepositoryTest.java | 11 +- src/test/java/bookrepo/util/TestUtil.java | 25 -- ...test.properties => application.properties} | 1 - .../database/add-categories-to-books.sql | 3 + .../database/book/add-books-to-table.sql | 2 + .../add-categories-to-category-table.sql | 3 + .../database/delete-data-from-tables.sql | 3 + 11 files changed, 345 insertions(+), 158 deletions(-) rename src/test/resources/{application-test.properties => application.properties} (99%) create mode 100644 src/test/resources/database/add-categories-to-books.sql create mode 100644 src/test/resources/database/book/add-books-to-table.sql create mode 100644 src/test/resources/database/category/add-categories-to-category-table.sql create mode 100644 src/test/resources/database/delete-data-from-tables.sql diff --git a/src/main/java/bookrepo/repository/book/BookRepository.java b/src/main/java/bookrepo/repository/book/BookRepository.java index e15232b..c350afd 100644 --- a/src/main/java/bookrepo/repository/book/BookRepository.java +++ b/src/main/java/bookrepo/repository/book/BookRepository.java @@ -2,9 +2,16 @@ import bookrepo.model.Book; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface BookRepository extends JpaRepository, JpaSpecificationExecutor { List findAllByCategories_Id(Long categoryId); + + @Query("SELECT b FROM Book b JOIN FETCH b.categories WHERE b.id = :id") + Optional findByIdWithCategories(@Param("id") Long id); + } diff --git a/src/test/java/bookrepo/BookRepoApplicationTests.java b/src/test/java/bookrepo/BookRepoApplicationTests.java index eb72a70..1f378f0 100644 --- a/src/test/java/bookrepo/BookRepoApplicationTests.java +++ b/src/test/java/bookrepo/BookRepoApplicationTests.java @@ -2,10 +2,8 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; @SpringBootTest -@ActiveProfiles("test") class BookRepoApplicationTests { @Test void contextLoads() { diff --git a/src/test/java/bookrepo/controller/BookControllerTest.java b/src/test/java/bookrepo/controller/BookControllerTest.java index 120ab07..cd98829 100644 --- a/src/test/java/bookrepo/controller/BookControllerTest.java +++ b/src/test/java/bookrepo/controller/BookControllerTest.java @@ -1,14 +1,11 @@ package bookrepo.controller; -import static bookrepo.util.TestUtil.createBookEffectiveJava; import static bookrepo.util.TestUtil.createBookRequestDto; -import static bookrepo.util.TestUtil.createExpectedBookDto; -import static bookrepo.util.TestUtil.createJavaCategory; -import static bookrepo.util.TestUtil.createProgrammingCategory; import static bookrepo.util.TestUtil.createValidBookRequestDto; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -18,6 +15,7 @@ import bookrepo.dto.book.BookDto; import bookrepo.dto.book.BookDtoWithoutCategoryIds; import bookrepo.dto.book.CreateBookRequestDto; +import bookrepo.mapper.BookMapper; import bookrepo.model.Book; import bookrepo.model.Category; import bookrepo.repository.book.BookRepository; @@ -25,28 +23,34 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.transaction.Transactional; +import java.math.BigDecimal; import java.util.List; -import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; - -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles("test") -@Transactional +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql(scripts = { + "classpath:database/delete-data-from-tables.sql", + "classpath:database/book/add-books-to-table.sql", + "classpath:database/category/add-categories-to-category-table.sql", + "classpath:database/add-categories-to-books.sql" +}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(scripts = "classpath:database/delete-data-from-tables.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) class BookControllerTest { - @Autowired - private MockMvc mockMvc; + protected static MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @@ -57,18 +61,31 @@ class BookControllerTest { @Autowired private CategoryRepository categoryRepository; + @Autowired + private BookMapper bookMapper; + + @BeforeAll + static void beforeAll(@Autowired WebApplicationContext applicationContext) { + mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext) + .apply(springSecurity()) + .build(); + } + + @AfterAll + static void afterAll(@Autowired BookRepository bookRepository, + @Autowired CategoryRepository categoryRepository) { + bookRepository.deleteAll(); + categoryRepository.deleteAll(); + } + @WithMockUser(username = "user", authorities = {"USER"}) @Test @DisplayName("Get all books") void findAll_WithUserRole_ShouldReturnPageOfBooks() throws Exception { // Given - Category categoryProgramming = categoryRepository.save(createProgrammingCategory()); - Category categoryJava = categoryRepository.save(createJavaCategory()); - Book book = createBookEffectiveJava(); - book.setCategories(Set.of(categoryProgramming, categoryJava)); - bookRepository.save(book); - - BookDto expectedDto = createExpectedBookDto(book); + BookDto expectedDto = bookRepository.findByIdWithCategories(1L) + .map(bookMapper::toDto) + .orElseThrow(); // When MvcResult result = mockMvc.perform(get("/books") @@ -87,8 +104,15 @@ void findAll_WithUserRole_ShouldReturnPageOfBooks() throws Exception { } ); - assertThat(books).hasSize(1); - assertThat(books.get(0).getTitle()).isEqualTo("Effective Java"); + assertThat(books).isNotEmpty(); + BookDto actualDto = books.get(0); + + assertThat(actualDto.getId()).isEqualTo(expectedDto.getId()); + assertThat(actualDto.getTitle()).isEqualTo("Effective Java"); + assertThat(actualDto.getAuthor()).isEqualTo("Joshua Bloch"); + assertThat(actualDto.getIsbn()).isEqualTo("9780134685991"); + assertThat(actualDto.getPrice()).isEqualTo(BigDecimal.valueOf(49.99)); + assertThat(actualDto.getCategoryIds()).containsExactlyInAnyOrder(1L, 2L); } @WithMockUser(username = "user", authorities = {"USER"}) @@ -96,33 +120,27 @@ void findAll_WithUserRole_ShouldReturnPageOfBooks() throws Exception { @DisplayName("Get book by ID") void getBookById_WithUserRole_ShouldReturnBook() throws Exception { // Given - Category category = categoryRepository.save(createProgrammingCategory()); - Book book = createBookEffectiveJava(); - book.setCategories(Set.of(category)); - Book savedBook = bookRepository.save(book); + BookDto expectedDto = bookRepository.findByIdWithCategories(1L) + .map(bookMapper::toDto) + .orElseThrow(); // When - MvcResult result = mockMvc.perform(get("/books/{id}", savedBook.getId())) + MvcResult result = mockMvc.perform(get("/books/{id}", expectedDto.getId())) .andExpect(status().isOk()) .andReturn(); // Then - BookDto actual = objectMapper.readValue( + BookDto actualDto = objectMapper.readValue( result.getResponse().getContentAsString(), BookDto.class ); - assertNotNull(actual); - assertEquals(savedBook.getTitle(), actual.getTitle()); - assertEquals(savedBook.getAuthor(), actual.getAuthor()); + assertEquals(expectedDto, actualDto); } @WithMockUser(username = "user", authorities = {"USER"}) @Test @DisplayName("Search books") void searchBooks_WithUserRole_ShouldReturnBooks() throws Exception { - // Given - bookRepository.save(createBookEffectiveJava()); - // When MvcResult result = mockMvc.perform(get("/books/search") .param("titles", "Effective Java") @@ -145,8 +163,7 @@ void searchBooks_WithUserRole_ShouldReturnBooks() throws Exception { @DisplayName("Create a new book") void createBook_ValidRequestDto_Success() throws Exception { // Given - Category categoryProgramming = categoryRepository.save(createProgrammingCategory()); - Category categoryJava = categoryRepository.save(createJavaCategory()); + long initialCount = bookRepository.count(); CreateBookRequestDto requestDto = createValidBookRequestDto(); String jsonRequest = objectMapper.writeValueAsString(requestDto); @@ -164,10 +181,11 @@ void createBook_ValidRequestDto_Success() throws Exception { result.getResponse().getContentAsString(), BookDto.class ); - assertNotNull(actual); - assertEquals(requestDto.getTitle(), actual.getTitle()); - assertEquals(requestDto.getAuthor(), actual.getAuthor()); - assertEquals(1, bookRepository.count()); + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("id", "categoryIds") + .isEqualTo(bookMapper.toDto(bookMapper.toModel(requestDto))); + } @WithMockUser(username = "admin", authorities = {"ADMIN"}) @@ -175,14 +193,15 @@ void createBook_ValidRequestDto_Success() throws Exception { @DisplayName("Delete book by ID") void deleteBookById_WithAdminRole_ShouldDeleteBook() throws Exception { // Given - Book book = bookRepository.save(createBookEffectiveJava()); + long initialCount = bookRepository.count(); + Book book = bookRepository.findAll().get(0); // When mockMvc.perform(delete("/books/{id}", book.getId())) .andExpect(status().isNoContent()); // Then - assertEquals(0, bookRepository.count()); + assertEquals(initialCount - 1, bookRepository.count()); } @WithMockUser(username = "admin", authorities = {"ADMIN"}) @@ -190,10 +209,9 @@ void deleteBookById_WithAdminRole_ShouldDeleteBook() throws Exception { @DisplayName("Update book") void updateBook_WithAdminRole_ShouldUpdateBook() throws Exception { // Given - Book book = bookRepository.save(createBookEffectiveJava()); + Book book = bookRepository.findAll().get(0); CreateBookRequestDto requestDto = createBookRequestDto(); requestDto.setTitle("Updated Title"); - String jsonRequest = objectMapper.writeValueAsString(requestDto); // When @@ -249,10 +267,7 @@ void accessWithoutAuthentication_ShouldReturnUnauthorized() throws Exception { @DisplayName("Get books by category ID") void getBooksByCategoryId_WithUserRole_ShouldReturnBooks() throws Exception { // Given - Category category = categoryRepository.save(createProgrammingCategory()); - Book book = createBookEffectiveJava(); - book.setCategories(Set.of(category)); - bookRepository.save(book); + Category category = categoryRepository.findAll().get(0); // When MvcResult result = mockMvc.perform(get("/categories/{id}/books", category.getId())) @@ -266,6 +281,173 @@ void getBooksByCategoryId_WithUserRole_ShouldReturnBooks() throws Exception { ); assertNotNull(books); assertEquals(1, books.length); - assertEquals(book.getTitle(), books[0].getTitle()); + assertEquals("Effective Java", books[0].getTitle()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get books by category ID - should return books for valid category") + void getBooksByCategoryId_WithValidCategory_ShouldReturnBooks() throws Exception { + // Given + Category category = categoryRepository.findAll().get(0); + Book expectedBook = bookRepository.findAll().get(0); + BookDtoWithoutCategoryIds expectedDto = bookMapper.toDtoWithoutCategories(expectedBook); + + // When + MvcResult result = mockMvc.perform(get("/categories/{id}/books", category.getId())) + .andExpect(status().isOk()) + .andReturn(); + + // Then + BookDtoWithoutCategoryIds[] books = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDtoWithoutCategoryIds[].class + ); + + assertNotNull(books); + assertEquals(1, books.length); + + assertThat(books[0]).usingRecursiveComparison().isEqualTo(expectedDto); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get books by invalid category ID - should return bad request") + void getBooksByCategoryId_WithInvalidCategoryId_ShouldReturnBadRequest() throws Exception { + // Given + String invalidCategoryId = "invalid"; + + // When & Then + mockMvc.perform(get("/categories/{id}/books", invalidCategoryId)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Get books by category ID without authentication - should return unauthorized") + void getBooksByCategoryId_WithoutAuthentication_ShouldReturnUnauthorized() throws Exception { + // Given + Category category = categoryRepository.findAll().get(0); + + // When & Then + mockMvc.perform(get("/categories/{id}/books", category.getId())) + .andExpect(status().isUnauthorized()); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get books by different categories - should return correct books") + void getBooksByCategoryId_WithDifferentCategories_ShouldReturnCorrectBooks() throws Exception { + // Given + Category programmingCategory = categoryRepository.findById(1L).orElseThrow(); + Category javaCategory = categoryRepository.findById(2L).orElseThrow(); + + // When & Then + MvcResult result1 = mockMvc.perform(get("/categories/{id}/books", + programmingCategory.getId())) + .andExpect(status().isOk()) + .andReturn(); + + MvcResult result2 = mockMvc.perform(get("/categories/{id}/books", + javaCategory.getId())) + .andExpect(status().isOk()) + .andReturn(); + + BookDtoWithoutCategoryIds[] books1 = objectMapper.readValue( + result1.getResponse().getContentAsString(), + BookDtoWithoutCategoryIds[].class + ); + + BookDtoWithoutCategoryIds[] books2 = objectMapper.readValue( + result2.getResponse().getContentAsString(), + BookDtoWithoutCategoryIds[].class + ); + + assertNotNull(books1); + assertNotNull(books2); + assertEquals(1, books1.length); + assertEquals(1, books2.length); + + Book expectedBook = bookRepository.findById(1L).orElseThrow(); + BookDtoWithoutCategoryIds expectedDto = bookMapper.toDtoWithoutCategories(expectedBook); + + assertThat(books1[0]).usingRecursiveComparison().isEqualTo(expectedDto); + assertThat(books2[0]).usingRecursiveComparison().isEqualTo(expectedDto); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get books by category ID with pagination - should return paged results") + void getBooksByCategoryId_WithPagination_ShouldReturnPagedResults() throws Exception { + // Given + Category category = categoryRepository.findAll().get(0); + + // When + MvcResult result = mockMvc.perform(get("/categories/{id}/books", category.getId()) + .param("page", "0") + .param("size", "5")) + .andExpect(status().isOk()) + .andReturn(); + + // Then + BookDtoWithoutCategoryIds[] books = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDtoWithoutCategoryIds[].class + ); + + assertNotNull(books); + assertEquals(1, books.length); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get book by ID - should return exact book") + void getBookById_WithUserRole_ShouldReturnExactBook() throws Exception { + // Given + BookDto expectedDto = bookRepository.findByIdWithCategories(1L) + .map(bookMapper::toDto) + .orElseThrow(); + + // When + MvcResult result = mockMvc.perform(get("/books/{id}", expectedDto.getId())) + .andExpect(status().isOk()) + .andReturn(); + + // Then + BookDto actualDto = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDto.class + ); + + assertThat(actualDto).usingRecursiveComparison().isEqualTo(expectedDto); + } + + @WithMockUser(username = "user", authorities = {"USER"}) + @Test + @DisplayName("Get all books - should return exact books list") + void findAll_WithUserRole_ShouldReturnExactBooks() throws Exception { + // Given + BookDto expectedDto = bookRepository.findByIdWithCategories(1L) + .map(bookMapper::toDto) + .orElseThrow(); + + // When + MvcResult result = mockMvc.perform(get("/books") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andReturn(); + + // Then + String responseContent = result.getResponse().getContentAsString(); + JsonNode root = objectMapper.readTree(responseContent); + List books = objectMapper.readValue( + root.get("content").toString(), + new TypeReference>() { + } + ); + + assertThat(books).hasSize(1); + + assertThat(books.get(0)).usingRecursiveComparison().isEqualTo(expectedDto); } } diff --git a/src/test/java/bookrepo/controller/CategoryControllerTest.java b/src/test/java/bookrepo/controller/CategoryControllerTest.java index e7a16f3..bcb1273 100644 --- a/src/test/java/bookrepo/controller/CategoryControllerTest.java +++ b/src/test/java/bookrepo/controller/CategoryControllerTest.java @@ -1,8 +1,9 @@ package bookrepo.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -10,45 +11,79 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import bookrepo.dto.category.CategoryDto; -import bookrepo.service.BookService; -import bookrepo.service.CategoryService; +import bookrepo.mapper.CategoryMapper; +import bookrepo.model.Category; +import bookrepo.repository.category.CategoryRepository; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Assertions; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; - -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles("test") +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql(scripts = { + "classpath:database/delete-data-from-tables.sql", + "classpath:database/category/add-categories-to-category-table.sql" +}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(scripts = "classpath:database/delete-data-from-tables.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) class CategoryControllerTest { - @Autowired - private MockMvc mockMvc; + protected static MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; - @MockBean - private CategoryService categoryService; + @Autowired + private CategoryRepository categoryRepository; + + @Autowired + private CategoryMapper categoryMapper; + + @BeforeAll + static void beforeAll(@Autowired WebApplicationContext applicationContext) { + mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext) + .apply(springSecurity()) + .build(); + } - @MockBean - private BookService bookService; + @AfterAll + static void afterAll(@Autowired CategoryRepository categoryRepository) { + categoryRepository.deleteAll(); + } @WithMockUser(username = "user", authorities = {"USER"}) @Test @DisplayName("Get all categories") void getAll_WithUserRole_ShouldReturnCategories() throws Exception { - mockMvc.perform(get("/categories")) - .andExpect(status().isOk()); + // Given + List categories = categoryRepository.findAll(); + CategoryDto expectedDto = categoryMapper.toDto(categories.get(0)); + + // When + MvcResult result = mockMvc.perform(get("/categories")) + .andExpect(status().isOk()) + .andReturn(); + + // Then + JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString()); + List actual = objectMapper.readValue( + root.get("content").toString(), + objectMapper.getTypeFactory().constructCollectionType(List.class, CategoryDto.class) + ); + assertThat(actual).isNotEmpty(); + assertThat(actual.get(0)).usingRecursiveComparison().isEqualTo(expectedDto); } @WithMockUser(username = "user", authorities = {"USER"}) @@ -56,26 +91,21 @@ void getAll_WithUserRole_ShouldReturnCategories() throws Exception { @DisplayName("Get category by ID") void getCategoryById_WithUserRole_ShouldReturnCategory() throws Exception { // Given - CategoryDto expected = new CategoryDto(); - expected.setId(1L); - expected.setName("Fiction"); - expected.setDescription("Fiction books"); - - when(categoryService.getById(1L)).thenReturn(expected); + Category category = categoryRepository.findAll().get(0); + CategoryDto expectedDto = categoryMapper.toDto(category); // When - MvcResult result = mockMvc.perform(get("/categories/1")) + MvcResult result = mockMvc.perform(get("/categories/{id}", category.getId())) .andExpect(status().isOk()) .andReturn(); // Then - CategoryDto actual = objectMapper.readValue(result - .getResponse() - .getContentAsString(), CategoryDto.class); - Assertions.assertNotNull(actual); - Assertions.assertEquals(expected.getId(), actual.getId()); - Assertions.assertEquals(expected.getName(), actual.getName()); - Assertions.assertEquals(expected.getDescription(), actual.getDescription()); + CategoryDto actualDto = objectMapper.readValue( + result.getResponse().getContentAsString(), + CategoryDto.class + ); + assertNotNull(actualDto); + assertThat(actualDto).usingRecursiveComparison().isEqualTo(expectedDto); } @WithMockUser(username = "admin", authorities = {"ADMIN"}) @@ -86,14 +116,7 @@ void createCategory_ValidRequestDto_Success() throws Exception { CategoryDto requestDto = new CategoryDto(); requestDto.setName("Science"); requestDto.setDescription("Science books"); - - CategoryDto expected = new CategoryDto(); - expected.setId(1L); - expected.setName("Science"); - expected.setDescription("Science books"); - String jsonRequest = objectMapper.writeValueAsString(requestDto); - when(categoryService.save(any(CategoryDto.class))).thenReturn(expected); // When MvcResult result = mockMvc.perform( @@ -105,13 +128,14 @@ void createCategory_ValidRequestDto_Success() throws Exception { .andReturn(); // Then - CategoryDto actual = objectMapper.readValue(result - .getResponse() - .getContentAsString(), CategoryDto.class); - Assertions.assertNotNull(actual); - Assertions.assertNotNull(actual.getId()); - Assertions.assertEquals(expected.getName(), actual.getName()); - Assertions.assertEquals(expected.getDescription(), actual.getDescription()); + CategoryDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + CategoryDto.class + ); + assertNotNull(actual); + assertNotNull(actual.getId()); + assertEquals(requestDto.getName(), actual.getName()); + assertEquals(requestDto.getDescription(), actual.getDescription()); } @WithMockUser(username = "admin", authorities = {"ADMIN"}) @@ -119,21 +143,15 @@ void createCategory_ValidRequestDto_Success() throws Exception { @DisplayName("Update category") void updateCategory_ValidRequestDto_Success() throws Exception { // Given + Category category = categoryRepository.findAll().get(0); CategoryDto requestDto = new CategoryDto(); requestDto.setName("Updated Fiction"); requestDto.setDescription("Updated description"); - - CategoryDto expected = new CategoryDto(); - expected.setId(1L); - expected.setName("Updated Fiction"); - expected.setDescription("Updated description"); - String jsonRequest = objectMapper.writeValueAsString(requestDto); - when(categoryService.update(eq(1L), any(CategoryDto.class))).thenReturn(expected); // When MvcResult result = mockMvc.perform( - put("/categories/1") + put("/categories/{id}", category.getId()) .content(jsonRequest) .contentType(MediaType.APPLICATION_JSON) ) @@ -141,28 +159,29 @@ void updateCategory_ValidRequestDto_Success() throws Exception { .andReturn(); // Then - CategoryDto actual = objectMapper.readValue(result - .getResponse() - .getContentAsString(), CategoryDto.class); - Assertions.assertNotNull(actual); - Assertions.assertEquals(expected.getName(), actual.getName()); - Assertions.assertEquals(expected.getDescription(), actual.getDescription()); + CategoryDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + CategoryDto.class + ); + assertNotNull(actual); + assertEquals(requestDto.getName(), actual.getName()); + assertEquals(requestDto.getDescription(), actual.getDescription()); } @WithMockUser(username = "admin", authorities = {"ADMIN"}) @Test @DisplayName("Delete category") void deleteCategory_WithAdminRole_ShouldDelete() throws Exception { - mockMvc.perform(delete("/categories/1")) + // Given + Category category = categoryRepository.findAll().get(0); + long initialCount = categoryRepository.count(); + + // When + mockMvc.perform(delete("/categories/{id}", category.getId())) .andExpect(status().isNoContent()); - } - @WithMockUser(username = "user", authorities = {"USER"}) - @Test - @DisplayName("Get books by category ID") - void getBooksByCategoryId_WithUserRole_ShouldReturnBooks() throws Exception { - mockMvc.perform(get("/categories/1/books")) - .andExpect(status().isOk()); + // Then + assertEquals(initialCount - 1, categoryRepository.count()); } @WithMockUser(username = "user", authorities = {"USER"}) @@ -172,7 +191,6 @@ void createCategory_WithUserRole_ShouldReturnForbidden() throws Exception { CategoryDto requestDto = new CategoryDto(); requestDto.setName("Test"); requestDto.setDescription("Test category"); - String jsonRequest = objectMapper.writeValueAsString(requestDto); mockMvc.perform( diff --git a/src/test/java/bookrepo/repository/BookRepositoryTest.java b/src/test/java/bookrepo/repository/BookRepositoryTest.java index f8b4e93..3e5bf62 100644 --- a/src/test/java/bookrepo/repository/BookRepositoryTest.java +++ b/src/test/java/bookrepo/repository/BookRepositoryTest.java @@ -1,27 +1,24 @@ package bookrepo.repository; +import static org.junit.jupiter.api.Assertions.assertEquals; + import bookrepo.model.Book; import bookrepo.model.Category; import bookrepo.repository.book.BookRepository; import bookrepo.repository.category.CategoryRepository; -import jakarta.transaction.Transactional; import java.math.BigDecimal; import java.util.List; import java.util.Set; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.testcontainers.junit.jupiter.Testcontainers; @SpringBootTest @ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@Transactional -@Testcontainers public class BookRepositoryTest { @Autowired @@ -49,7 +46,7 @@ void findAllByCategories_Id_finds1BookWithId1_ShouldReturnListWith1Book() { List actual = bookRepository.findAllByCategories_Id(savedCategory.getId()); - Assertions.assertEquals(1, actual.size()); - Assertions.assertEquals(savedBook.getId(), actual.get(0).getId()); + assertEquals(1, actual.size()); + assertEquals(savedBook.getId(), actual.get(0).getId()); } } diff --git a/src/test/java/bookrepo/util/TestUtil.java b/src/test/java/bookrepo/util/TestUtil.java index d61b03f..4d5cf58 100644 --- a/src/test/java/bookrepo/util/TestUtil.java +++ b/src/test/java/bookrepo/util/TestUtil.java @@ -108,31 +108,6 @@ public static BookDtoWithoutCategoryIds createBookDtoWithoutCategories(Book book return dto; } - public static BookDto createBookDto() { - BookDto dto = new BookDto(); - dto.setId(1L); - dto.setTitle("Test Book"); - dto.setAuthor("Test Author"); - dto.setIsbn("ISBN123"); - dto.setPrice(BigDecimal.valueOf(29.99)); - dto.setDescription("Test Description"); - dto.setCoverImage("cover.jpg"); - dto.setCategoryIds(Set.of(1L)); - return dto; - } - - public static BookDtoWithoutCategoryIds createBookDtoWithoutCategoryIds() { - BookDtoWithoutCategoryIds dto = new BookDtoWithoutCategoryIds(); - dto.setId(1L); - dto.setTitle("Test Book"); - dto.setAuthor("Test Author"); - dto.setIsbn("ISBN123"); - dto.setPrice(BigDecimal.valueOf(29.99)); - dto.setDescription("Test Description"); - dto.setCoverImage("cover.jpg"); - return dto; - } - public static CreateBookRequestDto createBookRequestDto() { CreateBookRequestDto dto = new CreateBookRequestDto(); dto.setTitle("Test Book"); diff --git a/src/test/resources/application-test.properties b/src/test/resources/application.properties similarity index 99% rename from src/test/resources/application-test.properties rename to src/test/resources/application.properties index db1affc..40bef53 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application.properties @@ -1,6 +1,5 @@ spring.datasource.url=jdbc:tc:mysql:8.0.37:///mydb spring.datasource.username=sa spring.datasource.password=password - jwt.expiration=300000 jwt.secret=MyJwtSecretKeyj98ty4j98hgj95j98hgj98hj diff --git a/src/test/resources/database/add-categories-to-books.sql b/src/test/resources/database/add-categories-to-books.sql new file mode 100644 index 0000000..fba3226 --- /dev/null +++ b/src/test/resources/database/add-categories-to-books.sql @@ -0,0 +1,3 @@ +insert into books_categories (book_id, category_id) values +(1, 1), +(1, 2); \ No newline at end of file diff --git a/src/test/resources/database/book/add-books-to-table.sql b/src/test/resources/database/book/add-books-to-table.sql new file mode 100644 index 0000000..8953df2 --- /dev/null +++ b/src/test/resources/database/book/add-books-to-table.sql @@ -0,0 +1,2 @@ +insert into books (id, title, author, isbn, price, description) values +(1, 'Effective Java', 'Joshua Bloch', '9780134685991', 49.99, 'A must-have book for every Java developer'); \ No newline at end of file diff --git a/src/test/resources/database/category/add-categories-to-category-table.sql b/src/test/resources/database/category/add-categories-to-category-table.sql new file mode 100644 index 0000000..408641c --- /dev/null +++ b/src/test/resources/database/category/add-categories-to-category-table.sql @@ -0,0 +1,3 @@ +insert into categories (id, name, description) values +(1, 'Programming', 'Books about programming and software development'), +(2, 'Java', 'Books specifically about Java programming language'); \ No newline at end of file diff --git a/src/test/resources/database/delete-data-from-tables.sql b/src/test/resources/database/delete-data-from-tables.sql new file mode 100644 index 0000000..2ff098f --- /dev/null +++ b/src/test/resources/database/delete-data-from-tables.sql @@ -0,0 +1,3 @@ +delete from books_categories; +delete from books; +delete from categories; \ No newline at end of file