[Practical Testing: 실용적인 테스트 가이드] 회고 4주차

출처 : Practical Testing: 실용적인 테스트 가이드

 

학습 내용 요약

섹션6 Spring & JPA 기반 테스트테스트케이스 세분화

Presentation Layer

  • 파라미터 검증

  • Persistence, Business Layer는 Mocking 처리 후 테스트

  • @Transactional

     

/*
* readOnly = true : 읽기 전용
* CRUD에서 CUD 동작 X / only Read
* JPA : CUD 스냅샷 저장, 변경감지 X (성능 향상)
* 
* CQRS - Command(CUD) / Read Query 분리
* Read 빈도가 훨씬 많음
* Read에 의해 CUD가 영향 받아도 안되고 CUD에 의해 Read Query가 영향 받아서도 안됨
* */
  • Dummy : 아무 것도 하지 않는 깡통 객체

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

  • stubbing(상태 검증)

    • 미리 준비한 결과를 제공하는 객체, 그 외에는 응답하지 않음

    • mock 객체의 행위에 원하는 응답 입력

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

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

     

 

  • @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이

    • @Mock

      • 타겟 객체의 껍데기를 생성

      • 다른 객체에 의존성을 주입하려면 해당 객체에 @InjectMocks 필요

      • 테스트 클래스에 @ExtendWith(MockitoExtension.class) 필요

        Mockito.when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))
                .thenReturn(true);
        
        BDDMockito.given(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))
                        .willReturn(true);
        
        BDDMockito는 Mockito.when이 given에 해당하는 내용이지만 네이밍이 when이로 발생하는 부조화 방지
    • @MockBean

      • 껍데기를 Bean으로 등록

      • 테스트 클래스에 @SpringBootTest, 주입 대상에 @Autowired 필요

    • @Spy

      • 객체의 기능 일부만 stub하고 나머지는 실제 기능 호출

      • 테스트 클래스에 @ExtendWith(MockitoExtension.class) 필요

        doReturn(true)
                .when(mailSendClient)
                .sendEmail(anyString(), anyString(), anyString(), anyString());
    • @SpyBean

      • @Spy Bean으로 등록

      • 테스트 클래스에 @SpringBootTest, 주입 대상에 @Autowired 필요

      • @SpyBean 대상이 인터페이스인 경우 구현체가 스프링 컨텍스트에 등록되어있어야함

    • @InjectMocks

      • @SpringBootTest가 아닌 경우 @Mock, @Spy 설정된 객체를 타겟 객체에 주입

Classicist vs Mockist

  • Classicist

    • 실제 기능 테스트 필요

    • 외부 시스템은 모킹처리 필요

    • 내부 기능 테스트라면 실제 기능 확인

    • Stubbing이 누락된 경우?

  • Mockist

    • 개별 레이어별로 모킹하여 테스트하면 충분

 

더 나은 테스트를 위한 구체적인 조언(섹션8)

한 문단에 한 주제

  • if 문 지양

  • 단위 테스트의 목적이 분명해야함

 

완벽하게 제어하기

  • 메서드 내에서 변경될 수 있는 값은 외부에서 주입하기

  • 테스트에서는 LocalDateTime.now() 지양 (영향도 검토 없이 무분별하게 사용되는 문제)

 

테스트 환경의 독립성 보장

  • given 절에서 테스트가 실패하지 않도록 구성하기

  • 생성자를 이용한 given절 구성 지향 (팩토리 메서드는 지양)

     

 

테스트 간 독립성 보장

  • 테스트별 공유자원 사용 지양 (테스트별 다른 given절 활용)

  • 테스트 순서에 무관하게 같은 결과 발생

  • 객체의 여러 상태 변경을 확인해야하는 테스트는 @DynamicTest 활용

 

한 눈에 들어오는 Test Fixture 구성

  • Test Fixture

    • 테스트를 위해 원하는 상태로 고정시킨 일련의 객체

    • given절에서 활용됨

    • Fixture 생성 메서드는 테스트에 필요한 메서드만 파라미터로 받기 (나머지는 고정값)

  • @BeforeAll

    • 모든 테스트 실행 전 1번 수행

  • @BeforeEach

    • 각 테스트 실행 전 수행

    • given절 생성 지양

      • 최하단 메서드에서 해당 내용 확인하려면 긴 스크롤 이동 필요

      • 모든 테스트에 영향 발생

    • 각 테스트 입장에서 아예 몰라도 테스트 이해하는 데에 문제가 없는 사항

    • 수정해도 모든 테스트에 영향을 주지 않는 코드

  • @AfterAll

    • 모든 테스트 실행 후 1번 수행

  • @AfterEach

    • 각 테스트 실행 후 수행

  • data.sql 등을 활용한 given 절 생성 지양 (테스트 파편화)

  • Test Fixture 생성 빌더 구성 클래스는 비추천 (난잡화)

 

Test Fixture 클렌징

  • deleteAll()

    • 테이블의 전체 데이터 조회 후 건별 삭제

    • 데이터가 많은 경우 성능 이슈 발생 가능성 있음

    • 순서 고려 필요

  • deleteAllInBatch()

    • delete from table

    • A테이블의 필드가 B테이블에 외래키로 활용되고 있는 경우 B테이블 데이터 삭제 후 A테이블 삭제 필요 (순서 중요)

  • @Transactional

    • Side Effect 고려 필요

 

@ParameterizedTest

  • 특정 데이터들을 바꿔가며 테스트하고 싶을 때 활용

  • CsvSource

@ParameterizedTest
@CsvSource({
    "HANDMADE, false",
    "BOTTLE, true" ,
    "BAKERY, true" ,
})
@DisplayName("재고타입인지 테스트")
public void test2(ProductType productType, boolean expected) {
    // given


    // when
    boolean result = ProductType.containsStockType(productType);

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

    • 메서드의 내용이 given에 해당하므로 테스트 위에 위치시킨다.

private static Stream<Arguments> ofProductTypes(){
    return Stream.of(
            Arguments.arguments(ProductType.HANDMADE, false),
            Arguments.arguments(ProductType.BOTTLE, true),
            Arguments.arguments(ProductType.BAKERY, true)
    );
}
@ParameterizedTest
@MethodSource("ofProductTypes")
@DisplayName("재고 관련 상품타입 확인")
void initializeGame(ProductType productType, boolean expected) {
    // given


    // when
    boolean result = ProductType.containsStockType(productType);

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

 

@DynamicTest

  • 공유 변수를 사용하여 테스트하는 것은 지양해야하지만 단계별 시나리오 테스트가 필요한 경우 활용

    @DisplayName("재고 차감 시나리오 테스트")
    @TestFactory
    Collection<DynamicTest> stockDeductionDynamicTest(){
        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("차감할 재고 수량이 없습니다.");
                })
        );
    }

 

테스트 수행도 비용이다. 환경 통합

  • @SpringBootTest는 상이한 환경마다 테스트 서버 기동이 필요하여 공통 환경을 추출하여 서버 기동 횟수 줄이기

  • @ActiveProfiles, @MockBean 등 공통 요소를 Layer별 구성 또는 필요에 따라 설정

    @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 service;
    }

 

class OrderControllerTest extends ControllerTestSupport {

    @Test
    ...
}

private 메서드 테스트는 어떻게?

  • 테스트 지양 - public 메서드를 통해서 검증하면 충분

  • 꼭 테스트 필요해보인다면 객체를 분리할 시점인지 고민

 

테스트에만 필요한 메서드는 어떻게? (프로덕션 코드에 필요없는 메서드)

  • Test Fixture 생성 메서드 등

  • 만들어도 되지만 최대한 지양해야함

 

부록(섹션9)

학습 테스트

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

  • 여러 테스트 케이스를 정의하고 검증하면서 구체적인 동작과 기능 학습 가능

  • 팀적으로 학습이 필요하다면 작성 후 공유

     

 

Spring REST Docs

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

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

  • AsciiDoc(MarkDown)을 사용하여 문서 작성됨

  • 테스트를 통과해야 문서가 만들어져 신뢰도가 높음

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

  • 코드 양이 많다.

  • 설정이 어렵다.

 

Swagger

  • 적용이 쉽다.

  • 문서에서 바로 API 호출 수행 가능

  • 프로덕션 코드에 침투적

  • 테스트와 무관하여 신뢰도가 떨어짐

댓글을 작성해보세요.

채널톡 아이콘