• 포스트
  • 도구
  • 작업
  • 포스트
  • 도구
  • 작업
로그인회원가입

© 2005 kaisa.co.kr. All Rights Reserved.

소개개인정보처리방침7083620@hanmail.net
← 목록으로
2025-10-20

테스트 — 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로 숨김 원인 제거

연습

  1. 서비스 단위 테스트 2개(성공/실패)를 작성한다.
  2. @WebMvcTest로 validation 400을 검증한다.
  3. Testcontainers PostgreSQL로 리포지토리 IT를 작성한다.
  4. @ServiceConnection으로 datasource가 붙는지 확인한다.

댓글

0

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

이전 글Spring Security 6 — 인증·인가·OAuth2·JWT다음 글Observability — Actuator, Micrometer, OpenTelemetry
← 목록으로