TestCode 치팅시트

최창효·2023년 10월 17일
0
post-custom-banner

엔티티 저장 확인

// given

// when
OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime);

// then
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
        .extracting("registeredDateTime", "totalPrice")
        .contains(registeredDateTime, 4000);
  1. Id가 Null이 아닌지 확인
  2. 변수에 원하는 값이 잘 들어갔는지 확인
  3. 변수에 List가 있다면 List값이 잘 들어갔는지 확인
    • size확인, extracting한 값 확인

List<객체> 확인

// given

// when
OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime);

// then
assertThat(orderResponse.getProducts()).hasSize(2)
        .extracting("productNumber","price")
        .containsExactlyInAnyOrder(
                Tuple.tuple("001",1000),
                Tuple.tuple("001",1000)
        );
  1. hasSize로 원하는 개수만큼 가져왔는지 확인
  2. list에 담긴 객체의 속성을 extracting으로 추출
  3. list에 담겼을 객체를 Tuple.tuple로 표현

List속 List

OrderResponse

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderResponse {
    Long orderId;
    List<OrderDetailResponse> orderDetailResponses;
}

OrderDetailResponse

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderDetailResponse {
    private Long orderDetailId;
    private Long price;
    private Long stock;
}

테스트 작성

// given // when
List<OrderResponse> contents;

// then
Assertions.assertThat(contents)
        .flatExtracting(orderResponse -> orderResponse.getOrderDetailResponses())
        .extracting("price","stock")
        .containsExactlyInAnyOrder(
        		Tuple.tuple(10000,100),
            	Tuple.tuple(20000,200)
        );
  1. flatExtracting으로 추출

정렬 순서

        // when
        List<Store> result = storeService.getStoresForAdmin(page, sort, sidoCode, gugunCode).getContent();

        // then
        assertThat(result).hasSize(3);
        IntStream.range(1, result.size()).forEach(i -> {
            Store prev = result.get(i-1);
            Store next = result.get(i);
            assertThat(prev.getCreatedAt().isAfter(next.getCreatedAt())).isTrue();
        });
  • for문 돌면서 앞뒤 검증

예외

// given

// when // then
assertThatThrownBy(() -> orderService.createOrder(request, registeredDateTime))
        	.isInstanceOf(IllegalArgumentException.class)
            .hasMessage("재고가 부족한 상품이 있습니다.");
  1. Assertions.assertThatThrownBy 사용
  2. isInstanceOf로 에러 타입 검증
  3. hasMessage로 에러 메시지 검증

@ParameterizedTest

MethodSource

private static Stream<Arguments> prodvideProductTypesForCheckingStockType(){
    return Stream.of(
        Arguments.of(ProductType.HANDMADE, false),
        Arguments.of(ProductType.BOTTLE, true),
        Arguments.of(ProductType.BAKERY, true)
    );
}

@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.")
@MethodSource("prodvideProductTypesForCheckingStockType")
@ParameterizedTest
void containsStockType4(ProductType productType, boolean expected){
    // when
    boolean result = ProductType.containsStockType(productType);

    // then
    assertThat(result).isEqualTo(expected);
}

EnumSource

@DisplayName("주문이 완료됐지만 결제가 성공하지 않았으면 결제 상태를 보여준다")
@ParameterizedTest
@EnumSource(value = PaymentStatus.class, names = {"READY","CANCELED","FAILED"}, mode = EnumSource.Mode.INCLUDE)
void enumTest(PaymentStatus paymentStatus) {
    // given
    OrderDetail orderDetail = createOrderDetail(paymentStatus);

    // when
    String result = orderDetail.getFinalStatusAsString();

    // then
    Assertions.assertThat(result).isEqualTo(paymentStatus.getMessage());

}
  • mode : Include는 names로 나열한 enum들만 변수로 넣고 테스트 진행. Exclude는 names로 나열한 enum을 제외한 것들을 변수로 넣고 테스트 진행

참고) enum값 테스트

ParameterizedTest말고 일반 테스트에서 eunm값을 비교하고 싶다면 .isEqualTo()가 아니라 isEqualByComparingTo()를 쓰면 된다

CsvSource

@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.")
@CsvSource({"HANDMADE,false","BOTTLE,true","BAKERY,true"})
@ParameterizedTest
void containsStockType3(ProductType productType, boolean expected){
    // when
    boolean result = ProductType.containsStockType(productType);

    // then
    assertThat(result).isEqualTo(expected);
}

@DynamicTest

기본 형태

@DisplayName("")
@TestFactory
Collection<DynamicTest> dynamicTest(){
    return List.of(
            DynamicTest.dynamicTest("Description",() -> {

            }),

            DynamicTest.dynamicTest("Description",() -> {

            })
    );
}
  1. @Test대신 @TestFactory를 사용
  2. 리턴값으로 iterable한 객체에 DynamicTest를 담아서 반환함
@DisplayName("재고 차감 시나리오")
@TestFactory
Collection<DynamicTest> stockDeductionDynamicTest(){
    // given
    Stock stock = Stock.create("001",1);

    return List.of(
            DynamicTest.dynamicTest("재고를 주어진 개수만큼 차가할 수 있다.",() -> {
                // given
                int quantity = 1;

                // when
                stock.deductQuantity(quantity);

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

            // 이전 시나리오에서 quantity가 1 차감된 상태가 여전히 유지되고 있음
            DynamicTest.dynamicTest("재고보다 많은 수의 수량으로 차감 시도하는 경우 예외가 발생한다",() -> {
                // given
                int quantity = 1;

                // when // then
                assertThatThrownBy(() -> stock.deductQuantity(quantity))
                        .isInstanceOf(IllegalArgumentException.class)
                        .hasMessage("차감할 재고 수량이 없습니다.");
            })
    );
}

모킹

@ExtendWith(MockitoExtension.class)
@SpringBootTest
@Transactional
class OrchestratorServiceTest {
    @MockBean
    private BeanA beanA;
    
    @Autowired BeanB beanB;
	
    @InjeckMocks
    private BeanC beanC; // beanA만 주입받으면 됨
	
    // 별도로 어노테이션 안붙음
    private BeanD beanD; // beanA와 beanB를 주입받아야 함
    
    @BeforeEach
    public void beforeEach(){
        beanD = new beanD(beanA,beanB);
    }
  • @ExtendWith(MockitoExtension.class) 선언
  • 모킹할 빈을 Test Double(Dummy, Fake, Stub, Spy, Mock)로 선언
  • 모킹할 빈을 주입받을 빈이 있다면 InjectMocks로 선언
  • 모킹할 빈, 그리고 일반 빈을 모두 주입받을 빈이 있다면 BeforeEach시점에 생성자로 전달

BDDMockito.given().willReturn()

BDDMockito.given(paymentFeignClient.kakaopayReady(BDDMockito.any()))
			.willReturn(ResponseEntity.ok().body("URL"));
  • 내가 해당 객체의 반환값을 지정할 수 있다

BDDMockito.given().willThrow()

BDDMockito.given(inventoryFeignClient.deductNormalQuantity(BDDMockito.any()))
		.willThrow(new RuntimeException());
  • 내가 해당 객체의 에러를 지정할 수 있다

BDDMockito.doNothing().when(모킹_객체).메서드()

BDDMockito.doNothing().when(kafkaProducer)
			.send(BDDMockito.any(),BDDMockito.any());
  • 모킹한 객체의 메서드가 호출됐을 때 아무런 동작을 하지 않도록 한다

BDDMockito.verify(모킹_객체).메서드()

BDDMockito.verify(cartFeignClient).removeItems(BDDMockito.any());
  • 모킹한 객체의 메서드가 호출됐는지 여부를 판단할 수 있다
  • A메서드 내부에서 B메서드를 호출했는지 확인할때 사용할 수 있다
    B메서드를 가진 객체(C)를 모킹하고 A를 실행한 뒤 verify(C).B메서드()를 하면 된다

컨트롤러

테스트 실행에 필요한 공통환경 세팅

@WebMvcTest(controllers = {
        OrderController.class,
        ProductController.class
})
public abstract class ControllerTestSupport {
    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @MockBean
    protected OrderService orderService;

    @MockBean
    protected ProductService productService;

}
  1. @WebMvcTest 선언
  2. 공통으로 사용되는 객체 주입
class ProductControllerTest extends ControllerTestSupport {

    @DisplayName("신규 상품을 등록할 때 상품 타입은 필수값이다.")
    @Test
    void createProductWithoutType() throws Exception{
        // given

        // when // then
        mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/products/new")
                .content(objectMapper.writeValueAsString(request)) // String 혹은 byte형태로 넣어주면 됨, 객체를 넣으려면 직렬화 과정이 필요
                .contentType(MediaType.APPLICATION_JSON)
       			)
                .andDo(MockMvcResultHandlers.print())
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("400"))
                .andExpect(jsonPath("$.status").value("BAD_REQUEST"))
                .andExpect(jsonPath("$.message").value("상품 타입은 필수입니다."))
                .andExpect(jsonPath("$.data").isEmpty());
    }
}
  1. 공통환경 상속받기
  2. perform으로 메서드 실행
    • 메서드 uri, 요청 데이터, contentType, 헤더 등을 설정
  3. 결과 확인
    • andDo로 로그 출력
    • andExpect로 검증 진행
    • jsonPath()로 값을 지정할 때 controller가 반환한 값 자체가 $가 된다. $.변수가 객체인 경우 $.변수.변수도 가능하다.
profile
기록하고 정리하는 걸 좋아하는 개발자.
post-custom-banner

0개의 댓글