[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 호출 수행 가능
프로덕션 코드에 침투적
테스트와 무관하여 신뢰도가 떨어짐
댓글을 작성해보세요.