워밍업 클럽 3기 BE 클린코드&테스트 - 4주차 발자국

워밍업 클럽 3기 BE 클린코드&테스트 - 4주차 발자국

강의 요약

섹션 7. Mock을 마주하는 자세

  • Test Double

    • Dummy: 아무 것도 하지 않는 깡통 객체

    • Fake: 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체 (ex. FakeRepository)

    • Stub: 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체 그 외에는 응답하지 않습니다. (상태 검증)

    • Spy: Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체 일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수 있습니다.

    • Mock: 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체 (행위 검증)

       

 

  • 순수 Mockito로 검증해보기

     

@Mock
private MailSendClient mailSendClient;
@Mock
private MailSendHistoryRepository mailSendHistoryRepository;
@InjectMocks
private MailService mailService;

@DisplayName("메일 전송 테스트")
@Test
void sendMail() {
    // given
    when(mailSendClient.sendMail(anyString(), anyString(), anyString(), anyString()))
        .thenReturn(true);

    // when
    boolean result = mailService.sendMail("", "", "", "");

    // then
    assertThat(result).isTrue();
    verify(mailSendHistoryRepository, times(1)).save(any(MailSendHistory.class));
}

 

  • BDDMockito

@Mock
private MailSendClient mailSendClient;
@Mock
private MailSendHistoryRepository mailSendHistoryRepository;
@InjectMocks
private MailService mailService;

@DisplayName("메일 전송 테스트")
@Test
void sendMail() {
    // given
    BDDMockito.given(mailSendClient.sendMail(anyString(), anyString(), anyString(), anyString()))
        .willReturn(true);

    // when
    boolean result = mailService.sendMail("", "", "", "");

    // then
    assertThat(result).isTrue();
    verify(mailSendHistoryRepository, times(1)).save(any(MailSendHistory.class));
}

 

  • Classicist

    • 장점:

      • 실제 객체를 사용하므로 테스트가 자연스럽고 직관적입니다.

      • 설계 변경 시 테스트가 깨질 가능성이 적어 리팩토링이 용이합니다.

      • 도메인 모델 중심으로 설계가 진행되어 UI와 비즈니스 로직이 분리됩니다.

    • 단점:

      • 복잡한 협력자와의 통합 테스트에서 추가 작업이 필요할 수 있습니다.

      • 상태 검증만으로는 일부 동작 관련 오류를 놓칠 가능성이 있습니다.

         

 

  • Mockis

    • 장점:

      • 모든 협력자를 Mock으로 대체하여 세부적인 동작을 명확히 검증 가능합니다.

      • 복잡한 협력자와의 상호작용을 쉽게 테스트할 수 있습니다.

    • 단점:

      • 구현 세부사항에 강하게 의존하여 리팩토링 시 테스트가 깨질 가능성이 높습니다.

      • 지나치게 세분화된 테스트는 통합 오류를 놓칠 수 있습니다.

 

섹션 8. 더 나은 테스트를 작성하기 위한 구체적 조언

  • 한 문단에 한 주제!

     

    • 테스트 하나 당 목적은 하나여야 합니다.

    • if, for 사용 지양

 

  • 완벽하게 제어하기

    • LocalDateTime.now() 사용 지양

 

  • 테스트 환경의 독립성을 보장하자

    • 공유 변수와 테스트 목적이 아닌 메소드 사용 지양

 

  • 한 눈에 들어오는 Test Fixture 구성하기

    • Test Fixture

      • 테스트를 위해 원하는 상태로 고정시킨 일련의 객체 즉, given절에서 생성했던 모든 객체들을 의미

    • BeforeEach, BeforeAll, AfterEach, AfterAll

      • 각 테스트가 픽스처의 내부 구현을 몰라도 테스트 내용을 이해하는 데 문제가 없을 경우에 사용해야 합니다.

    • Given 절에서 SQL 사용 지양

      • SQL로 Given 객체를 생성하면 테스트 코드의 가독성이 떨어지고, 무엇을 테스트하려는지 명확하지 않을 수 있습니다. 이는 테스트 목적을 파편화시키는 결과를 초래합니다.

         

 

  • Test Fixture 클렌징

     

    • deleteAll()

      • 모든 엔티티를 하나씩 순차적으로 삭제합니다.

      • 내부적으로 for 루프를 돌며 각각의 엔티티에 대해 개별 DELETE 쿼리를 실행합니다.

      • 대량의 데이터를 삭제할 경우 성능이 저하됩니다. (100만 개의 데이터를 삭제하면 100만 번의 쿼리가 실행됩니다.)

         

    • deleteAllInBatch()

      • 삭제할 모든 엔티티를 하나의 SQL DELETE 쿼리로 처리합니다.

      • DELETE FROM table_name과 같은 단일 쿼리를 실행하여 성능이 훨씬 뛰어납니다.

      • 대량 데이터 삭제 시에도 단일 쿼리로 처리되므로 성능이 우수합니다. (동일한 데이터 삭제 시 단 한 번의 쿼리만 실행됩니다)

    • @ParameterizedTest

      • 미리 정의된 값(ProductType)을 사용해 반복적으로 테스트를 실행합니다.

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

    // then
    assertThat(result).isEqualTo(expected);
}
private static Stream<Arguments> provideProductTypesForCheckingStockType() {
    return Stream.of(
        Arguments.of(ProductType.HANDMADE, false),
        Arguments.of(ProductType.BOTTLE, true),
        Arguments.of(ProductType.BAKERY, true)
    );
}

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

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

    • @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();
        }),
        DynamicTest.dynamicTest("재고보다 많은 수의 수량으로 차감 시도하는 경우 예외가 발생한다.", () -> {
            // given
            int quantity = 1;

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

 

  • 수행 환경 통합하기

    • @ActiveProfiles("test"),

      @SpringBootTest 어노테이션을 사용하는 추상 클래스를 생성하여 상속받는 구조로 구현을 해야 실행 횟수가 줄어듭니다.

 

  • private method test

    • 작성하지 않지는 않고, 객체를 분리할 시점인지를 확인해야 합니다.

 

  • 테스트에서만 필요한 코드

    • 만들어도 되지만, 보수적으로 접근해야 합니다.

 

섹션 9. Appendix

  • 학습 테스트

    • 잘 모르는 기능, 라이브러리, 프레임워크를 학습하기 위해 작성하는 테스트

    • 여러 테스트 케이스를 스스로 정의하고 검증하는 과정을 통해 보다 구체적인 동작과 기능을 학습할 수 있습니다.

    • 관련 문서만 읽는 것보다 훨씬 재미있게 학습할 수 있습니다.

 

  • Spring REST Docs

    • 테스트 코드를 통한 API 문서 자동화 도구

    • API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원활하게 합니다.

    • 기본적으로 AsciiDoc을 사용하여 문서를 작성합니다.

    • REST Docs

      • 장점:

        • 테스트를 통과해야 문서가 만들어집니다. (신뢰도가 높다.)

        • 프로덕션 코드에 비침투적입니다.

      • 단점:

        • 코드 양이 많습니다.

        • 설정이 어렵습니다.

    • Swagger

      • 장점:

        • 적용이 쉽습니다.

        • 문서에서 바로 API 호출을 수행해볼 수 있습니다.

      • 단점:

        • 프로덕션 코드에 침투적입니다.

        • 테스트와 무관하기 때문에 신뢰도가 떨어질 수 있습니다.

 

미션 (DAY16)

미션 (DAY18)

  • @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이 및 @BeforeEach, given절, when절 적절한 배치

     

  • 결과:

    https://inf.run/HxEwT


발작국 4주차 회고

이번 주는 테스트 코드 작성과 관련된 다양한 어노테이션의 차이를 이해하고 이를 실무에 어떻게 적용할 수 있을지 고민하는 시간이었습니다. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks의 차이점을 명확히 이해하는 데 집중했으며 각각의 어노테이션이 어떤 상황에서 적합한지 학습할 수 있었습니다. 더불어 테스트 코드를 작성하며 효율성을 높이는 방법에 대해서도 많은 것을 배웠습니다. 특히 Mock 객체를 활용해 데이터베이스나 네트워크 호출과 같은 외부 시스템 의존성을 줄이는 것이 얼마나 중요한지 체감할 수 있었으며 테스트 코드를 효과적으로 작성하는 방법에 대해 다시 한번 깊이 생각하게 된 의미 있는 시간이었습니다.

댓글을 작성해보세요.

채널톡 아이콘