
워밍업 클럽 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)
Layered Architecture의 특징 및 테스트 작성
미션 (DAY18)
@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이 및 @BeforeEach, given절, when절 적절한 배치
결과:
발작국 4주차 회고
이번 주는 테스트 코드 작성과 관련된 다양한 어노테이션의 차이를 이해하고 이를 실무에 어떻게 적용할 수 있을지 고민하는 시간이었습니다. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks의 차이점을 명확히 이해하는 데 집중했으며 각각의 어노테이션이 어떤 상황에서 적합한지 학습할 수 있었습니다. 더불어 테스트 코드를 작성하며 효율성을 높이는 방법에 대해서도 많은 것을 배웠습니다. 특히 Mock 객체를 활용해 데이터베이스나 네트워크 호출과 같은 외부 시스템 의존성을 줄이는 것이 얼마나 중요한지 체감할 수 있었으며 테스트 코드를 효과적으로 작성하는 방법에 대해 다시 한번 깊이 생각하게 된 의미 있는 시간이었습니다.
댓글을 작성해보세요.