Test Code - dnd-side-project/dnd-11th-5-backend GitHub Wiki

Warning

ν˜„μž¬ @Transactional 둜 μ˜ν•œ νŠΈλžœμž­μ…˜ 경계 κ΄€λ ¨ μ΄μŠˆκ°€ μ‘΄μž¬ν•¨!

κ΄€λ ¨ pr : https://github.com/dnd-side-project/dnd-11th-5-backend/pull/155

참고 자료

[IntelliJ] Live Template 생성

Guide

μ°Έκ³  자료의 λ§Žμ€ 뢀뢄을 μ°Έκ³ ν•˜μ˜€μœΌλ―€λ‘œ, ν•΄λ‹Ή 자료λ₯Ό λ¨Όμ € μ½λŠ” 것을 μΆ”μ²œ

κΈ°λ³Έ ν…ŒμŠ€νŠΈ μ „λž΅

given / when / then

  • [IntelliJ] Live Template 생성 을 톡해 μžλ™μœΌλ‘œ μ½”λ“œλ₯Ό 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.

  • given : ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ λͺ¨λ“  μ€€λΉ„ 단계

  • when : μ–΄λ–€ ν–‰μœ„λ₯Ό ν–ˆμ„ λ•Œ

  • then : μ–΄λ– ν•œ κ²°κ³Όκ°€ λ‚˜μ™€μ•Ό ν•œλ‹€.

μ˜ˆμ‹œ

@DisplayName("μ£Όλ¬Έ 생성 μ‹œ μ£Όλ¬Έ 등둝 μ‹œκ°„μ„ κΈ°λ‘ν•œλ‹€.")
@Test
public void registeredDateTime() {
    // given
    LocalDateTime registeredDateTime = LocalDateTime.now();
    List<Product> products = List.of(
       createProduct("001", 1000),
       createProduct("002", 2000)
    );

    // when
    Order order = Order.create(products, registeredDateTime);

    // then
    assertThat(order.getRegisteredDateTime()).isEqualTo(registeredDateTime);
}

Unit ν…ŒμŠ€νŠΈ

  • 주둜 도메인 ν…ŒμŠ€νŠΈ μ§„ν–‰μ‹œ μ‚¬μš©
  • ν•΄ν”Ό μΌ€μ΄μŠ€μ™€ μ˜ˆμ™Έ μΌ€μ΄μŠ€ 두 가지 κ²½μš°μ— λŒ€ν•΄ λͺ¨λ‘ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•œλ‹€.
    • 경계값 ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•œλ‹€.

μ˜ˆμ‹œ

@DisplayName("재고λ₯Ό 주어진 개수만큼 차감할 수 μžˆλ‹€.")
@Test
public void deductQuantity() {
    // given
    Stock stock = Stock.create("001", 1);
    int quantity = 1;

    // when
    stock.deductQuantity(quantity);

    // then
    assertThat(stock.getQuantity()).isZero();
}

@DisplayName("μž¬κ³ λ³΄λ‹€ λ§Žμ€ 수의 μˆ˜λŸ‰μœΌλ‘œ 차감 μ‹œλ„ν•˜λŠ” 경우 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.")
@Test
public void deductQuantity2() {
    // given
    Stock stock = Stock.create("001", 1);
    int quantity = 2;

    // when
    // then    assertThatThrownBy(() -> stock.deductQuantity(quantity))
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("차감할 재고 μˆ˜λŸ‰μ΄ μ—†μŠ΅λ‹ˆλ‹€.");
}

톡합 ν…ŒμŠ€νŠΈ

  • IntergrationTestSupport λ₯Ό extends ν•˜μ—¬ μ‚¬μš©ν•˜λ‚˜, mocking λ“±μœΌλ‘œ μ„œλΉ„μŠ€ ν™˜κ²½μ΄ 달라진닀면 λ”°λ‘œ μž‘μ„±ν•œλ‹€.
  • 참고자료 : λ”μ¦ˆ, ν‹°ν‚€μ˜ Classic TDD VS Mockist TDD
    • classic 으둜 주둜 μ§„ν–‰ν•˜λ˜, μ„œλΉ„μŠ€ ν…ŒμŠ€νŠΈμ—μ„œ λ‹€λ₯Έ μ„œλΉ„μŠ€λ‚˜ ν˜Ήμ€ λ„ˆλ¬΄ λ³΅μž‘ν•œ λ°μ΄ν„°μ…‹νŒ…μ΄ μžˆλ‹€λ©΄ mokup ν•œλ‹€.
      • MockupTestSupport λ₯Ό extends ν•œλ‹€.

λ ˆμ΄μ–΄ 별 ν…ŒμŠ€νŠΈ μ „λž΅

컨트둀러 ν…ŒμŠ€νŠΈ

  • 검증해야 ν•˜λŠ” 것 : client μ—μ„œ λ„˜μ–΄μ˜¨ 값에 λŒ€ν•œ validation
    • ν•˜μœ„ λ ˆμ΄μ–΄λ₯Ό mocking ν•˜μ—¬ μ²˜λ¦¬ν•œλ‹€.
  • ControllerTestSupport λ₯Ό extends ν•˜μ—¬ μ‚¬μš©ν•œλ‹€.
@ActiveProfiles("test")
@WebMvcTest(controllers = {
    HelloController.class, // μ‚¬μš©ν•˜λŠ” 컨트둀러 여기에 μΆ”κ°€
})
public abstract class ControllerTestSupport {
    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    // λͺ¨ν‚Ήν•  빈 μΆ”κ°€
    // @MockBean
    // protected ProductService productService;
}

μ˜ˆμ‹œ μ½”λ“œ

class ProductControllerTest extends ControllerTestSupport {

    @DisplayName("μ‹ κ·œ μƒν’ˆμ„ λ“±λ‘ν•œλ‹€.")
    @Test
    public void createProduct() throws Exception {
       // given
       ProductCreateRequest request = ProductCreateRequest.builder()
          .type(ProductType.HANDMADE)
          .sellingStatus(ProductSellingStatus.SELLING)
          .name("레λͺ¬μ—μ΄λ“œ")
          .price(4000)
          .build();

       // when // then
       mockMvc.perform(
             post("/api/v1/products/new")
                .content(objectMapper.writeValueAsString(request))
                .contentType(MediaType.APPLICATION_JSON)
          )
          .andDo(print()) // μžμ„Έν•œ 둜그 보기
          .andExpect(status().isOk());
    }

    @DisplayName("μ‹ κ·œ μƒν’ˆμ„ 등둝할 λ•Œ μƒν’ˆ νƒ€μž…μ€ ν•„μˆ˜κ°’μ΄λ‹€.")
    @Test
    public void createProductWithoutType() throws Exception {
       // given
       ProductCreateRequest request = ProductCreateRequest.builder()
          .sellingStatus(ProductSellingStatus.SELLING)
          .name("레λͺ¬μ—μ΄λ“œ")
          .price(4000)
          .build();

       // when // then
       mockMvc.perform(
             post("/api/v1/products/new")
                .content(objectMapper.writeValueAsString(request))
                .contentType(MediaType.APPLICATION_JSON)
          )
          .andDo(print()) // μžμ„Έν•œ 둜그 보기
          .andExpect(status().isBadRequest())
          // μ‘λ‹΅μœΌλ‘œ λ‚΄λ €μ˜¨ json 객체 κ²€μ¦ν•˜κΈ°
          .andExpect(jsonPath("$.code").value("400"))
          .andExpect(jsonPath("$.status").value("BAD_REQUEST"))
          .andExpect(jsonPath("$.message").value("μƒν’ˆ νƒ€μž…μ€ ν•„μˆ˜μž…λ‹ˆλ‹€."))
          .andExpect(jsonPath("$.data").isEmpty())
       ;
    }

    @DisplayName("μ‹ κ·œ μƒν’ˆμ„ 등둝할 λ•Œ μƒν’ˆ 이름은 ν•„μˆ˜κ°’μ΄λ‹€.")
    @Test
    void createProductWithoutName() throws Exception {
       // given
       ProductCreateRequest request = ProductCreateRequest.builder()
          .type(ProductType.HANDMADE)
          .sellingStatus(ProductSellingStatus.SELLING)
          .price(4000)
          .build();

       // when // then
       mockMvc.perform(
             post("/api/v1/products/new")
                .content(objectMapper.writeValueAsString(request))
                .contentType(MediaType.APPLICATION_JSON)
          )
          .andDo(print())
          .andExpect(status().isBadRequest())
          .andExpect(jsonPath("$.code").value("400"))
          .andExpect(jsonPath("$.status").value("BAD_REQUEST"))
          .andExpect(jsonPath("$.message").value("μƒν’ˆ 이름은 ν•„μˆ˜μž…λ‹ˆλ‹€."))
          .andExpect(jsonPath("$.data").isEmpty())
       ;
    }

    @DisplayName("μ‹ κ·œ μƒν’ˆμ„ 등둝할 λ•Œ μƒν’ˆ 가격은 μ–‘μˆ˜μ΄λ‹€.")
    @Test
    public void createProductWithZeroPrice() throws Exception {
       // given
       ProductCreateRequest request = ProductCreateRequest.builder()
          .type(ProductType.HANDMADE)
          .sellingStatus(ProductSellingStatus.SELLING)
          .name("레λͺ¬μ—μ΄λ“œ")
          .price(0)
          .build();

       // when // then
       mockMvc.perform(
             post("/api/v1/products/new")
                .content(objectMapper.writeValueAsString(request))
                .contentType(MediaType.APPLICATION_JSON)
          )
          .andDo(print()) // μžμ„Έν•œ 둜그 보기
          .andExpect(status().isBadRequest())
          // μ‘λ‹΅μœΌλ‘œ λ‚΄λ €μ˜¨ json 객체 κ²€μ¦ν•˜κΈ°
          .andExpect(jsonPath("$.code").value("400"))
          .andExpect(jsonPath("$.status").value("BAD_REQUEST"))
          .andExpect(jsonPath("$.message").value("μƒν’ˆ 가격은 μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."))
          .andExpect(jsonPath("$.data").isEmpty())
       ;
    }

    @DisplayName("판맀 μƒν’ˆμ„ μ‘°νšŒν•œλ‹€.")
    @Test
    public void getSellingProducts() throws Exception {
       // given
       List<ProductResponse> result = List.of();
       when(productService.getSellingProducts()).thenReturn(result);

       // when // then
       mockMvc.perform(
             get("/api/v1/products/selling")
          )
          .andDo(print())
          .andExpect(status().isOk())
          .andExpect(jsonPath("$.code").value("200"))
          .andExpect(jsonPath("$.status").value("OK"))
          .andExpect(jsonPath("$.message").value("OK"))
          // λ‹€λ₯Έ 뢀뢄은 service와 repository μ—μ„œ 검증이 λ˜μ—ˆμœΌλ―€λ‘œ
          // controller μ—μ„œλŠ” μ›ν•˜λŠ” ν˜•νƒœ, (μ—¬κΈ°μ„ ) Array ν˜•νƒœμΈμ§€ μ •λ„λ‘œ κ²€μ¦ν•œλ‹€.
          .andExpect(jsonPath("$.data").isArray());
    }
}

μ„œλΉ„μŠ€ ν…ŒμŠ€νŠΈ

  • κ°€λŠ₯ν•˜λ‹€λ©΄ mockup 없이 μ§„ν–‰ν•œλ‹€.
    • κ·ΈλŸ¬λ‚˜ λ‹€λ₯Έ μ„œλΉ„μŠ€ ν˜Ήμ€ λ„ˆλ¬΄ λ³΅μž‘ν•œ 데이터 μ…‹νŒ…μ΄λΌλ©΄ mockup ν•œλ‹€.
  • mocking 없이 μ§„ν–‰ν•œλ‹€λ©΄ IntergrationTestSupport λ₯Ό extends ν•˜μ—¬ μ‚¬μš©ν•˜κ³ , mocking ν•˜μ—¬ μ‚¬μš©ν•œλ‹€λ©΄ ν•΄λ‹Ή ν΄λž˜μŠ€μ— λŒ€ν•΄μ„œλŠ” λ”°λ‘œ ν™˜κ²½μ„ μ„€μ •ν•œλ‹€.

πŸ”₯ μ£Όμ˜μ‚¬ν•­

각 ν…ŒμŠ€νŠΈμ— λŒ€ν•œ rollback 편의λ₯Ό μœ„ν•΄, IntergrationTestSupport 에 @Transactional μ• λ…Έν…Œμ΄μ…˜μ„ μΆ”κ°€ν•˜μ˜€μœΌλ‚˜ 이 경우 service layerμ—μ„œ νŠΈλžœμž­μ…˜μ„ λΉΌλ¨Ήμ—ˆμ„ 경우 에 μ£Όμ˜ν•˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.

리포지토리 ν…ŒμŠ€νŠΈ

  • 직접 μ§  query, queryDSL 에 λŒ€ν•œ ν…ŒμŠ€νŠΈλ₯Ό μš°μ„ μœΌλ‘œ ν•œλ‹€.
  • κ²°κ³Όλ³΄λ‹€λŠ” 쿼리가 μ–΄λ–»κ²Œ λ‚˜κ°€λŠ”μ§€ ν™•μΈν•˜λŠ” κ±Έ μš°μ„ μœΌλ‘œ ν•œλ‹€.
⚠️ **GitHub.com Fallback** ⚠️