테스트 — JUnit 5, MockMvc, Testcontainers
테스트 — JUnit 5, MockMvc, Testcontainers
요약: 단위·슬라이스·통합 테스트 비중을 나누고, Testcontainers로 실제 DB에 가까운 검증을 한다.
1. 왜 피라미드를 나누는가
모든 케이스를 @SpringBootTest로 올리면 CI가 느리고 실패 지점이 흐려진다. 빠른 테스트로 규칙을 지키고, 조립은 소수 통합으로 확인한다.
| 종류 | 도구 | 목적 |
|---|---|---|
| 단위 | JUnit + Mockito | 도메인·서비스 규칙 |
| 슬라이스 | @WebMvcTest, @DataJpaTest |
계층 경계 |
| 통합 | @SpringBootTest + Testcontainers |
실제 조립 |
| E2E | RestAssured 등 | 핵심 시나리오만 |
슬라이스 테스트는 웹 또는 JPA처럼 한 계층만 띄우는 테스트다.
2. 단위 테스트
Spring 컨텍스트 없이 생성자로 목을 넣는다.
class OrderServiceTest {
@Test
void rejectsEmptyItems() {
OrderRepository repo = mock(OrderRepository.class);
OrderService service = new OrderService(repo);
assertThatThrownBy(() -> service.create(new CreateOrderRequest(List.of())))
.isInstanceOf(InvalidOrderException.class);
}
}
필드 @Autowired 서비스는 이런 테스트가 불편해진다.
3. @WebMvcTest
컨트롤러·검증·예외 핸들러만 올린다. 서비스는 목이다. Boot 3.4+는 @MockitoBean(@MockBean 후속)을 쓴다.
@WebMvcTest(UserController.class)
@Import(GlobalExceptionHandler.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean UserService userService;
@Test
void createUser() throws Exception {
given(userService.create(any()))
.willReturn(new UserResponse(1L, "a@b.com", "Ann"));
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"email":"a@b.com","name":"Ann"}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("a@b.com"));
}
}
Security가 켜져 있으면 @WithMockUser 또는 @AutoConfigureMockMvc(addFilters = false)를 명시한다. 필터를 끄면 인가 버그를 놓친다.
4. @DataJpaTest와 Testcontainers
한 줄 정의: Testcontainers는 테스트 동안 Docker로 PostgreSQL 같은 실제 미들웨어를 띄운다. H2는 방언이 달라 운영에서만 깨지는 쿼리가 나온다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfig {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgres() {
return new PostgreSQLContainer<>("postgres:16-alpine");
}
}
@ServiceConnection(Boot 3.1+)이 datasource URL을 붙인다. @DynamicPropertySource보다 짧다.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(TestcontainersConfig.class)
class UserRepositoryTest {
@Autowired UserRepository userRepository;
@Test
void findByEmail() {
userRepository.save(User.create("a@b.com", "Ann"));
assertThat(userRepository.findByEmail("a@b.com")).isPresent();
}
}
Replace.ANY(기본)면 H2로 바뀌어 컨테이너를 띄운 의미가 없다.
5. @SpringBootTest
전체 컨텍스트가 필요할 때만 쓴다. HTTP까지 보려면 RANDOM_PORT + TestRestTemplate 또는 MockMvc(@AutoConfigureMockMvc)다. Boot 3.4+에는 RestTestClient도 있다.
@SpringBootTest
@Import(TestcontainersConfig.class)
class UserServiceIT {
@Autowired UserService userService;
@Test
void createAndGet() {
UserResponse created = userService.create(new CreateUserRequest("a@b.com", "Ann"));
assertThat(userService.get(created.id()).email()).isEqualTo("a@b.com");
}
}
Kafka·Redis도 같은 @ServiceConnection 패턴이다.
6. 데이터·시간·외부 의존성
테스트마다 독립이어야 한다. @Transactional 롤백, 또는 컨테이너 재사용 + 테이블 정리. @Sql은 데이터가 단순할 때만 쓴다.
static User user(String email) {
return User.create(email, email.split("@")[0]);
}
AssertJ 체인이 JUnit assertEquals보다 실패 메시지가 읽기 쉽다.
assertThat(result)
.extracting(UserResponse::email, UserResponse::name)
.containsExactly("a@b.com", "Ann");
| 대상 | 기법 |
|---|---|
| 시간 | Clock Bean을 고정 |
| 외부 HTTP | WireMock / MockWebServer |
| 메시지 | Testcontainers Kafka |
| 파일 | @TempDir |
@Bean
@Primary
Clock fixedClock() {
return Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
}
7. CI
러너에 Docker가 있어야 Testcontainers가 돈다. 로컬 재사용은 testcontainers.reuse.enable=true. 실패한 테스트를 재시도로 통과시키지 않는다.
8. 흔한 실수
| 실수 | 대안 |
|---|---|
전부 @SpringBootTest |
단위·슬라이스 먼저 |
@DataJpaTest + H2로 복잡한 SQL |
Testcontainers + Replace.NONE |
| Security 필터를 항상 끔 | 인가 케이스는 @WithMockUser |
now() 직접 호출 |
Clock 주입 |
flaky를 @RepeatedTest로 숨김 |
원인 제거 |
연습
- 서비스 단위 테스트 2개(성공/실패)를 작성한다.
@WebMvcTest로 validation 400을 검증한다.- Testcontainers PostgreSQL로 리포지토리 IT를 작성한다.
@ServiceConnection으로 datasource가 붙는지 확인한다.
댓글
0아직 댓글이 없습니다. 첫 댓글을 남겨보세요.