Spring Boot 테스트 작성 회고

오병택·2026년 2월 9일
post-thumbnail

WebMvcTest 컨트롤러 테스트부터 JWT 단위 테스트, 그리고 설정/시크릿 관리까지

오늘은 컨트롤러 테스트를 붙이면서 생긴 문제를 해결하고, JWT 신규 플로우 테스트 방향(단위/통합)까지 정리했다. 과정에서 Spring Boot 4 변경점(@MockBean 제거 등)과 JPA Auditing이 @WebMvcTest를 깨는 케이스, ObjectMapper import 실수 같은 실전 트러블슈팅도 같이 정리했다.

1. 테스트 분류 감 잡기: 단위 vs 슬라이스 vs 통합

단위(Unit)

  • 스프링 없이, 의존성은 mock, 클래스 로직 자체 검증

예: Service 분기/예외, 도메인 상태전이, 유틸 계산

슬라이스(Slice)

  • 레이어 하나만 스프링으로 얇게 띄움

예: @WebMvcTest로 Controller + Validation + ExceptionHandler

통합(Integration)

  • 실제로 여러 레이어/설정/필터/DB까지 붙여 전체 흐름 검증

예: Security 필터 + EntryPoint + Controller 연동 확인

2. 컨트롤러 테스트 기본 패턴(MockMvc)

  • 컨트롤러 테스트는 “HTTP 요청/응답 레이어”만 검증하는 게 핵심이다.

MockMvc 체인 해석

mockMvc.perform(post("/api/auth/signup")
        .contentType(MediaType.APPLICATION_JSON)       // 요청 Content-Type: JSON
        .content(objectMapper.writeValueAsString(req))) // request body JSON
    .andExpect(status().isCreated())                   // 201 검증
    .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) // JSON 응답 검증
    .andExpect(jsonPath("$.id").value(1L));            // 응답 JSON 필드 검증

import 정리

given

import static org.mockito.BDDMockito.given;

status()

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

실무 세트

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

3. SpringBoot 4.xx 에서 달라진 점들

(1) WebMvcTest/AutoConfigureMockMvc 패키지

  • Spring Boot 4에서는 webmvc 패키지로 이동하는 케이스가 있어 관련 import가 안 잡힐 수 있다.

(2) @MockBean이 import 안 됨

  • SpringBoot 4.xx에서 @MockBean/@SpyBean이 제거(deprecate → removal)되면서 대체 애노테이션을 써야 한다.

대체

@MockitoBean / @MockitoSpyBean

4. @WebMvcTest가 ApplicationContext 로딩 실패한 이유: JPA Auditing

에러

JPA metamodel must not be empty
  • jpaAuditingHandler / jpaMappingContext 관련 BeanCreationException

원인

  • 메인 클래스에 @EnableJpaAuditing이 붙어있어서
    @WebMvcTest 같은 “웹 슬라이스 테스트”에서도 Auditing이 켜짐

  • 그런데 @WebMvcTest는 JPA 엔티티 메타모델을 준비하지 않으므로 충돌 발생

해결

  • Auditing을 별도 Config로 분리

메인 클래스에서 제거

@ConfigurationPropertiesScan
@SpringBootApplication
public class DeliveryPlatformApplication { ... }

별도 설정 클래스로 이동

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig { }

의도

  • 실서비스/통합 테스트에서는 Auditing 사용

  • @WebMvcTest에서는 JPA 인프라를 강제 로딩하지 않게 분리

5. ObjectMapper가 null로 터진 이유: “Testcontainers shaded ObjectMapper” import 실수

NPE 메시지

org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper

원인

  • IDE 자동 import가 testcontainers 내부 shaded ObjectMapper를 잡아서
    스프링의 Jackson ObjectMapper 빈 주입이 타입 불일치로 실패 → null

해결

import com.fasterxml.jackson.databind.ObjectMapper;

6. Mockito에서 ArgumentMatchers.any()를 쓰는 이유

  • 컨트롤러 테스트에서 request DTO를 직접 만들어도,
    실제 컨트롤러 파라미터는 JSON 역직렬화로 새 객체가 들어온다.

그래서

given(authService.signup(req)).willReturn(res);

는 객체 동일성/equals에 의해 매칭이 깨질 수 있다.

따라서

given(authService.signup(any(SignupRequest.class))).willReturn(res);

처럼 타입 매칭으로 안정화.

추가로 전달값 검증이 필요하면:

  • ArgumentCaptor 또는 argThat 사용

7. ErrorResponse의 HttpStatus가 “같아 보이는데” 테스트 실패한 이유

응답 JSON

"httpStatus":"409 CONFLICT"

-> 즉, 서버 내부에선 HttpStatus.CONFLICT(enum)이지만 JSON 직렬화 시 문자열 "409 CONFLICT"로 나간다.

따라서 테스트에서 enum을 직접 비교하면 타입이 달라서 실패할 수 있다.

해결

문자열로 검증하거나, status는 status().isConflict()로 검증하고 바디는 errorCode/message만 확인

8. JWT 만료시간 테스트 방법

가장 안정적인 검증은 exp - iat 비교

  • iat 발급 시간

  • exp 만료 시간

long diffMillis = exp - iat;
long expectedMillis = Duration.ofMinutes(expireMinutes).toMillis();
assertThat(diffMillis).isBetween(expectedMillis - 1000, expectedMillis + 1000);

또한 subject는 String.valueOf(1L)이므로 "1"이다. "1L"이 아니다.

AssertJ import

import static org.assertj.core.api.Assertions.assertThat;
profile
걱정하지 말고 일단 해봐!

0개의 댓글