유닛 테스트를 작성할 때에는 @SpringBootTest
로 전체 스프링 부트를 띄우게끔 하지 않고 가볍게 테스트하려고 이것저것 찾아보면서 적용해보았다.
@DataJpaTest
+ @TestEntityManager
Spring data Jpa를 테스트할 때 사용
인메모리 db를 생성해서 entity들만 스캔, 테스트 후 자동 롤백
@DataJpaTest
class ProductOptionRepositoryTest {
@Autowired
private ProductOptionRepository productOptionRepository;
@Autowired
private TestEntityManager entityManager;
private List<String> option1s, option2s;
private Product product;
private List<ProductOption> productOptions;
@BeforeEach
void setUp() {
option1s = new ArrayList<>() {{
add("블랙");
add("네이비");
}};
option2s = new ArrayList<>() {{
add("M");
add("L");
add("XL");
}};
product = new Product("후드티");
productOptions = new ArrayList<>() {
{
add(new ProductOption(product, 10, option1s.get(0), option2s.get(0)));
add(new ProductOption(product, 20, option1s.get(0), option2s.get(1)));
add(new ProductOption(product, 30, option1s.get(0), option2s.get(2)));
add(new ProductOption(product, 40, option1s.get(1), option2s.get(1)));
add(new ProductOption(product, 50, option1s.get(1), option2s.get(2)));
}
};
entityManager.persistAndFlush(product);
productOptions.forEach(productOption -> entityManager.persistAndFlush(productOption));
}
@DisplayName("제품의 옵션1로 모든 옵션 조회")
@Test
void findAllByProductAndOption1() {
// when
List<ProductOption> all = productOptionRepository.findAllByProductAndOption1(product, option1s.get(1));
// then
assertAll(
() -> assertThat(all).hasSize(2),
() -> assertThat(all).extracting("option2")
.containsOnly(option2s.get(1), option2s.get(2))
);
}
}
@ExtendWith(MockitoExtension.class)
+ @Mock
+ @InjectMocks
@Mock
or Mockito.mock(A.class)
: mocking할 repository
@Injectmocks
: 모킹한 객체를 사용할 객체 - service
@ExtendWith(MockitoExtension.class)
class ProductOptionServiceMockingTest {
@Mock
private ProductOptionRepository productOptionRepository;
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductOptionService productOptionService;
private Product product;
private Long productId;
private ProductOption productOption;
private int stock;
private String option1;
private String option2;
@BeforeEach
void setUp() {
product = new Product("바지");
productId = 1L;
stock = 10;
option1 = "블랙";
option2 = "M";
productOption = ProductOption.builder()
.product(product)
.stock(stock)
.option1(option1)
.option2(option2)
.build();
productOption.setCreatedAt(LocalDateTime.now());
productOption.setUpdatedAt(LocalDateTime.now());
Mockito.lenient().when(productRepository.findById(productId))
.thenReturn(Optional.of(product));
}
@Test
@DisplayName("제품 옵션 구매시 재고 부족 예외")
void purchaseProductOption() {
// given
int purchasedNum = 15;
when(productOptionRepository.findById(any()))
.thenReturn(Optional.of(productOption));
// when, then
assertThatThrownBy(() -> productOptionService.purchase(anyLong(), purchasedNum))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("재고가 부족합니다. product option ")
.hasMessageContaining("의 재고: " + stock);
}
}
@WebMvcTest(ProductOptionController.class)
+ MockMvc
+ RestDocs
Web 관련된 설정과 클래스들만 컨텍스트로 띄워주는 기능
@WebMvcTest(ProductOptionController.class)
class ProductOptionControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private ProductOptionService productOptionService;
private Long productId;
private ProductSimpleDto productDto;
private Long productOptionId;
private ProductOptionResponse productOptionResponse;
@BeforeEach
void setUp() {
productId = 1L;
productDto = ProductSimpleDto.builder()
.id(productId)
.name("바지")
.price(1000)
.status("판매 중")
.build();
productOptionId = 1L;
productOptionResponse = ProductOptionResponse.builder()
.id(productOptionId)
.productDto(productDto)
.stock(20)
.option1("흑청")
.option2("30")
.createdAt(LocalDateTime.now().toString())
.updatedAt(LocalDateTime.now().toString())
.build();
}
@Test
@DisplayName("제품 옵션 생성")
void createProductOption() throws Exception {
// given
ProductOptionRequest productOptionRequest = ProductOptionRequest.builder()
.productDto(productDto)
.stock(20)
.option1("흑청")
.option2("30")
.build();
String requestBody = objectMapper.writeValueAsString(productOptionRequest);
given(productOptionService.create(anyLong(), any()))
.willReturn(productOptionResponse);
// when
ResultActions resultActions = mockMvc.perform(post("/api/v1/products/{productId}/productOptions", productId)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andDo(print());
// then
resultActions
.andExpect(status().isCreated())
.andExpect(header().string(
LOCATION, "/api/v1/products/" + productId + "/productOptions/" + productOptionResponse.getId()))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("id").value(productOptionId))
.andExpect(jsonPath("productDto").exists())
.andExpect(jsonPath("productDto.id").value(productId))
.andExpect(jsonPath("productDto.name").value("바지"))
.andExpect(jsonPath("stock").value(20))
.andExpect(jsonPath("option1").value("흑청"))
.andExpect(jsonPath("option2").value("30"))
.andDo(print());
}
}
@SpringBootTest
+ TestRestTemplate
전체 컨텍스트를 띄워서 클라이언트에서 요청을 보내듯이 테스트해볼 수 있음
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ProductOptionTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ProductOptionRepository productOptionRepository;
@Autowired
private ProductRepository productRepository;
private Long productId;
private Product savedProduct;
private String url;
@BeforeEach
void setUp() {
restTemplate.getRestTemplate().setRequestFactory(new HttpComponentsClientHttpRequestFactory());
productOptionRepository.deleteAll();
productRepository.deleteAll();
Product product = Product.builder()
.name("청바지")
.price(1000)
.category("하의")
.build();
savedProduct = productRepository.saveAndFlush(product);
productId = savedProduct.getId();
url = "/api/v1/products/" + productId + "/productOptions";
}
@Test
@DisplayName("옵션명 변경")
void updateOptionName() {
// given
ProductOption saved = saveProductOption(10, "블랙");
ProductOptionNameRequest request = new ProductOptionNameRequest("네이비", "L");
HttpEntity<ProductOptionNameRequest> requestEntity = new HttpEntity<>(request);
// when
ResponseEntity<ProductOptionResponse> response = restTemplate.exchange(
url + "/" + saved.getId() + "/optionName",
HttpMethod.PATCH,
requestEntity,
ProductOptionResponse.class);
// then
assertAll(
() -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK),
() -> assertThat(response.getBody()).isNotNull(),
() -> assertThat(response.getBody()).hasFieldOrPropertyWithValue("option1", "네이비"),
() -> assertThat(response.getBody()).hasFieldOrPropertyWithValue("option2", "L")
);
}
private ProductOption saveProductOption(int stock, String option1) {
ProductOption productOption = ProductOption.builder()
.product(savedProduct)
.stock(stock)
.option1(option1)
.build();
return productOptionRepository.saveAndFlush(productOption);
}
}