Skip to content

Commit

Permalink
test(product): add web layer tests for product package
Browse files Browse the repository at this point in the history
  • Loading branch information
korzhhiik committed Sep 10, 2023
1 parent 473fc52 commit b8bfee1
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

Expand Down Expand Up @@ -45,7 +46,7 @@ private List<String> collectErrorMessages(Exception exception) {
.getBindingResult()
.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList() : List.of(exception.getMessage());
.toList() : Collections.singletonList(exception.getMessage());
}

private String buildErrorDescription(Exception exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ public ProductInfoDto getProductById(final UUID productId) {
return new ProductNotFoundException(productId);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class ProductNotFoundException extends RuntimeException {
private final UUID productId;

public ProductNotFoundException(final UUID productId) {
super(String.format("The product with productId = %s is not found.",productId));
super(String.format("The product with productId = %s is not found.", productId));
this.productId = productId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.zufar.onlinestore.product.exception.handler;

import com.zufar.onlinestore.common.exception.handler.GlobalExceptionHandler;
import com.zufar.onlinestore.common.response.ApiResponse;
import com.zufar.onlinestore.product.exception.ProductNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RequiredArgsConstructor
@RestControllerAdvice
@Slf4j
public class ProductExceptionHandler extends GlobalExceptionHandler {

@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiResponse<Void> handleProductNotFoundException(final ProductNotFoundException exception) {
ApiResponse<Void> apiResponse = buildResponse(exception, HttpStatus.NOT_FOUND);
log.error("Handle product not found exception: failed: messages: {}, description: {}.",
apiResponse.messages(), apiResponse.description());

return apiResponse;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package com.zufar.onlinestore.product.endpoint;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zufar.onlinestore.common.response.ApiResponse;
import com.zufar.onlinestore.product.api.ProductApi;
import com.zufar.onlinestore.product.dto.ProductInfoDto;
import com.zufar.onlinestore.product.dto.ProductListWithPaginationInfoDto;
import com.zufar.onlinestore.product.exception.ProductNotFoundException;
import com.zufar.onlinestore.security.jwt.filter.JwtAuthenticationProvider;
import org.instancio.Instancio;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.UUID;

import static com.zufar.onlinestore.product.endpoint.ProductsEndpoint.PRODUCTS_URL;
import static com.zufar.onlinestore.product.util.ProductUtilStub.buildSampleProducts;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(controllers = ProductsEndpoint.class, excludeAutoConfiguration = {SecurityAutoConfiguration.class})
class ProductsEndpointTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@MockBean
private ProductApi productApi;

@MockBean
private JwtAuthenticationProvider provider;

private ProductInfoDto productInfo;

private ProductListWithPaginationInfoDto productList;

@Test
void whenGetProductsSortedByNameDescThenReturnProducts() throws Exception {
int page = 0;
int size = 10;
String sortAttribute = "name";
Sort.Direction sortDirection = Sort.Direction.DESC;
ProductListWithPaginationInfoDto products = buildSampleProducts(page, size, sortAttribute, sortDirection);

when(productApi.getProducts(page, size, sortAttribute, sortDirection.name())).thenReturn(products);

mockMvc.perform(get(PRODUCTS_URL)
.param("page", String.valueOf(page))
.param("size", String.valueOf(size))
.param("sort_attribute", sortAttribute)
.param("sort_direction", sortDirection.name())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.products[0].name").value("Product E"))
.andExpect(jsonPath("$.totalElements").value(products.totalElements()));

verify(productApi).getProducts(page, size, sortAttribute, sortDirection.name());
}

@Nested
class ProductsEndpointTests {
@BeforeEach
void setUp() {
productInfo = Instancio.create(ProductInfoDto.class);
productList = buildSampleProducts(0, 10, "name", Sort.Direction.ASC);
}

@Test
void whenGetProductByIdThenReturn200() throws Exception {
UUID productId = UUID.randomUUID();

when(productApi.getProduct(productId)).thenReturn(productInfo);

mockMvc.perform(get(PRODUCTS_URL + "/{productId}", productId)
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(productInfo.id().toString()));

verify(productApi).getProduct(productId);
}

@Test
void whenGetProductsThenReturn200() throws Exception {
int page = 1;
int size = 10;
String sortAttribute = "name";
String defaultDirection = Sort.Direction.ASC.name();

when(productApi.getProducts(page, size, sortAttribute, defaultDirection)).thenReturn(productList);

mockMvc.perform(get(PRODUCTS_URL)
.param("page", String.valueOf(page))
.param("size", String.valueOf(size))
.param("sort_attribute", sortAttribute)
.param("sort_direction", defaultDirection)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.products.size()").value(productList.products().size()));

verify(productApi).getProducts(page, size, sortAttribute, defaultDirection);
}

@Test
void whenNullProductIdThenReturn404() throws Exception {
UUID productId = UUID.randomUUID();
String errorDescription = buildErrorDescription(
ProductsEndpoint.class.getName()
);

when(productApi.getProduct(productId)).thenThrow(new ProductNotFoundException(productId));

MvcResult mvcResult = mockMvc.perform(get(PRODUCTS_URL + "/{productId}", productId))
.andExpect(status().isNotFound())
.andReturn();

String actualResponse = mvcResult.getResponse().getContentAsString();
ApiResponse<Void> expectedResponse = createExpectedErrorResponse(errorDescription, productId);
String expectedResponseBody = objectMapper.writeValueAsString(expectedResponse);

assertThat(actualResponse).isEqualTo(expectedResponseBody);

verify(productApi).getProduct(productId);
}

@Test
void whenGetProductsSortedByNameAscThenReturnProducts() throws Exception {
int page = 0;
int size = 10;
String sortAttribute = "name";
Sort.Direction sortDirection = Sort.Direction.ASC;

when(productApi.getProducts(page, size, sortAttribute, sortDirection.name())).thenReturn(productList);

mockMvc.perform(get(PRODUCTS_URL)
.param("page", String.valueOf(page))
.param("size", String.valueOf(size))
.param("sort_attribute", sortAttribute)
.param("sort_direction", sortDirection.name())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.products[0].name").value("Product A"))
.andExpect(jsonPath("$.totalElements").value(productList.totalElements()));

verify(productApi).getProducts(page, size, sortAttribute, sortDirection.name());
}

@Test
void whenFirstPageRetrievedThenReturn200() throws Exception {
int page = 0;
int size = 10;

when(productApi.getProducts(page, size, null, null)).thenReturn(productList);

mockMvc.perform(get(PRODUCTS_URL)
.param("page", String.valueOf(page))
.param("size", String.valueOf(size))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page").value(page));

verify(productApi).getProducts(page, size, null, null);
}

private String buildErrorDescription(String className) {
final int problematicCodeLine = 33;
return String.format("Operation was failed in method: %s that belongs to the class: %s. Problematic code line: %d",
"getProductById", className, problematicCodeLine);
}

private ApiResponse<Void> createExpectedErrorResponse(String errorDescription, UUID productId) {
return new ApiResponse<>(
null,
Collections.singletonList(String.format("The product with productId = %s is not found.", productId)),
errorDescription,
HttpStatus.NOT_FOUND.value(),
LocalDateTime.now());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.zufar.onlinestore.product.util;

import com.zufar.onlinestore.product.dto.ProductInfoDto;
import com.zufar.onlinestore.product.dto.ProductListWithPaginationInfoDto;
import org.springframework.data.domain.Sort;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;

public class ProductUtilStub {

public static ProductListWithPaginationInfoDto buildSampleProducts(Integer page, Integer size, String sortAttribute, Sort.Direction sortDirection) {
List<ProductInfoDto> products = generateSampleProducts();
List<ProductInfoDto> sortedProducts = sortProducts(products, sortAttribute, sortDirection);

return new ProductListWithPaginationInfoDto(sortedProducts, page, size, (long) sortedProducts.size(), calculateTotalPages(products.size(), size));
}

private static List<ProductInfoDto> generateSampleProducts() {
List<ProductInfoDto> products = new ArrayList<>();

products.add(new ProductInfoDto(
UUID.randomUUID(),
"Product A",
"Description for Product A",
BigDecimal.valueOf(10.50),
20
));

products.add(new ProductInfoDto(
UUID.randomUUID(),
"Product B",
"Description for Product B",
BigDecimal.valueOf(15.75),
50
));

products.add(new ProductInfoDto(
UUID.randomUUID(),
"Product C",
"Description for Product C",
BigDecimal.valueOf(20.00),
30
));

products.add(new ProductInfoDto(
UUID.randomUUID(),
"Product D",
"Description for Product D",
BigDecimal.valueOf(5.25),
40
));

products.add(new ProductInfoDto(
UUID.randomUUID(),
"Product E",
"Description for Product E",
BigDecimal.valueOf(12.00),
60
));
return products;
}

private static List<ProductInfoDto> sortProducts(List<ProductInfoDto> products, String sortAttribute, Sort.Direction direction) {
Comparator<ProductInfoDto> comparator = switch (sortAttribute.toLowerCase()) {
case "name" -> Comparator.comparing(ProductInfoDto::name);
case "description" -> Comparator.comparing(ProductInfoDto::description);
case "price" -> Comparator.comparing(ProductInfoDto::price);
case "quantity" -> Comparator.comparing(ProductInfoDto::quantity);
default -> throw new IllegalArgumentException("Unsupported sort attribute: " + sortAttribute);
};

if (direction == Sort.Direction.DESC) {
comparator = comparator.reversed();
}
return products.stream().sorted(comparator).toList();
}

private static Integer calculateTotalPages(Integer totalElements, Integer size) {
return (int) Math.ceil((double) totalElements / size);
}
}

0 comments on commit b8bfee1

Please sign in to comment.