Contents
see List개요
JUnit 5와 Mockito는 Java 단위 테스트의 사실상 표준 조합입니다. JUnit 5는 JUnit Platform, Jupiter, Vintage로 구성된 모듈형 아키텍처를 채택하며, 확장 모델과 다양한 테스트 패턴을 제공합니다. Mockito 5는 Java 21과 완벽히 호환되며 더 강력한 모킹 기능을 갖추고 있습니다. 이 글에서는 실무에서 즉시 적용할 수 있는 효과적인 테스트 작성법을 다룹니다.
핵심 개념
효과적인 단위 테스트의 핵심 원칙과 도구입니다.
- FIRST 원칙: Fast(빠르게), Isolated(격리), Repeatable(반복 가능), Self-validating(자동 검증), Timely(적시)
- Given-When-Then: 테스트 구조를 준비-실행-검증으로 명확히 분리합니다.
- Parameterized Test: 동일 로직을 다양한 입력으로 반복 테스트합니다.
- Nested Test: 테스트를 계층적으로 구성하여 가독성을 높입니다.
- Mock vs Stub vs Spy: 테스트 대역(Test Double)의 종류를 구분하여 적절히 사용합니다.
실전 예제
JUnit 5의 고급 기능과 Mockito를 결합한 테스트 예제입니다.
// 주문 서비스 테스트 - Nested + Parameterized + Mockito
@ExtendWith(MockitoExtension.class)
@DisplayName("OrderService 테스트")
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private InventoryService inventoryService;
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Nested
@DisplayName("주문 생성")
class CreateOrder {
@Test
@DisplayName("정상적인 주문이 성공적으로 생성된다")
void shouldCreateOrderSuccessfully() {
// Given
var request = new OrderRequest("PROD-001", 2, "user-123");
when(inventoryService.checkStock("PROD-001", 2)).thenReturn(true);
when(paymentGateway.charge(any())).thenReturn(PaymentResult.success("PAY-001"));
when(orderRepository.save(any())).thenAnswer(inv -> {
Order order = inv.getArgument(0);
return order.withId("ORD-001");
});
// When
Order result = orderService.createOrder(request);
// Then
assertAll("주문 생성 결과",
() -> assertThat(result.id()).isEqualTo("ORD-001"),
() -> assertThat(result.status()).isEqualTo(OrderStatus.CONFIRMED),
() -> assertThat(result.productId()).isEqualTo("PROD-001"),
() -> assertThat(result.quantity()).isEqualTo(2)
);
// 상호작용 검증
verify(inventoryService).reserveStock("PROD-001", 2);
verify(paymentGateway).charge(argThat(p -> p.amount() > 0));
}
@Test
@DisplayName("재고 부족 시 예외가 발생한다")
void shouldThrowWhenOutOfStock() {
// Given
var request = new OrderRequest("PROD-001", 100, "user-123");
when(inventoryService.checkStock("PROD-001", 100)).thenReturn(false);
// When & Then
assertThatThrownBy(() -> orderService.createOrder(request))
.isInstanceOf(InsufficientStockException.class)
.hasMessageContaining("재고 부족");
verify(paymentGateway, never()).charge(any());
}
}
@Nested
@DisplayName("주문 금액 계산")
class CalculatePrice {
@ParameterizedTest(name = "수량 {0}개, 단가 {1}원 -> 총액 {2}원")
@CsvSource({
"1, 10000, 10000",
"5, 10000, 50000",
"10, 10000, 90000", // 10개 이상 10% 할인
"20, 10000, 160000" // 20개 이상 20% 할인
})
@DisplayName("수량별 금액 계산이 올바르다")
void shouldCalculatePriceWithDiscount(int qty, int unitPrice, long expected) {
long result = orderService.calculateTotal(qty, unitPrice);
assertThat(result).isEqualTo(expected);
}
}
}
BDD 스타일과 커스텀 확장 예제입니다.
// BDDMockito를 활용한 BDD 스타일 테스트
import static org.mockito.BDDMockito.*;
@Test
@DisplayName("사용자 프로필 조회 - BDD 스타일")
void shouldReturnUserProfile() {
// Given
given(userRepository.findById("user-123"))
.willReturn(Optional.of(new User("user-123", "홍길동")));
given(orderRepository.countByUserId("user-123"))
.willReturn(15L);
// When
UserProfile profile = userService.getProfile("user-123");
// Then
then(userRepository).should().findById("user-123");
assertThat(profile.name()).isEqualTo("홍길동");
assertThat(profile.orderCount()).isEqualTo(15L);
}
// 커스텀 테스트 확장 - 실행 시간 측정
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TimingExtension.class)
@interface Timed { long maxMillis() default 1000; }
class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
@Override
public void beforeTestExecution(ExtensionContext ctx) {
ctx.getStore(Namespace.GLOBAL).put("start", System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext ctx) {
long start = ctx.getStore(Namespace.GLOBAL).get("start", long.class);
long elapsed = System.currentTimeMillis() - start;
System.out.printf("[%s] 실행 시간: %dms%n", ctx.getDisplayName(), elapsed);
}
}
활용 팁
@DisplayName을 항상 사용하여 테스트 목적을 명확히 표현하세요. 한국어로 작성하면 비즈니스 요구사항과 직접 대응됩니다.@Nested로 테스트를 기능별로 그룹화하면 테스트 리포트의 가독성이 크게 향상됩니다.- Mockito의
verify는 필요한 상호작용만 검증하세요. 과도한 검증은 테스트를 깨지기 쉽게 만듭니다. - AssertJ의
assertThat은 JUnit의assertEquals보다 가독성이 뛰어나고 풍부한 검증 API를 제공합니다. @ParameterizedTest는 경계값 분석, 동등 분할 테스트에 특히 효과적입니다.
마무리
효과적인 단위 테스트는 코드 품질의 핵심 지표입니다. JUnit 5의 확장 모델과 Mockito의 강력한 모킹 기능을 결합하면, 읽기 쉽고 유지보수가 용이한 테스트를 작성할 수 있습니다. 테스트 커버리지 수치보다 중요한 것은 핵심 비즈니스 로직과 경계 조건을 빠짐없이 검증하는 것입니다. 테스트를 작성하는 시간은 결국 디버깅 시간을 절약해줍니다.