WebMvcTest + Spring REST doc 을 적용
적용 이유: 테스트와 동시에 문서화를 자동화 할 수 있기에 문서 작성의 편리함
Spring REST doc을 선택한 이유: Swagger와 달리, 프로덕트 코드에 테스트 관련 코드가 포함되지 않음. 이후 Swagger UI만 가져와서, Spring REST doc과 Swagger의 장점을 합칠 예정
RestAssured가 아니라 MockMvc를 선택한 이유:
RestAssured는 @SpringBootTest를 실행해야 하기 때문에 속도가 느림.
@WebMvcTest(GreetingController.class)는
Spring에서 웹 계층(Controller) 만 테스트하기 위한 전용 어노테이션입니다.
속도는 빠르면서도 HTTP 요청/응답 흐름을 실제처럼 테스트할 수 있다는 장점이 있어요.
✅ @WebMvcTest란?
@SpringBootTest가 전체 애플리케이션 컨텍스트를 로딩하는 반면,
@WebMvcTest는 웹 관련 컴포넌트만 로딩해서 테스트를 수행합니다.
🔍 @WebMvcTest(GreetingController.class) 의미 상세 분석
@WebMvcTest | Spring MVC Controller 계층만 테스트하겠다는 선언 |
(JoinController.class) | 테스트 대상 Controller를 명시 (다른 Controller는 로딩하지 않음) |
⚠️ 지정하지 않으면 모든 @Controller, @RestController가 로딩되므로 성능 저하가 생길 수 있습니다.
✅ 로딩되는 컴포넌트
- @Controller, @RestController (지정된 것만)
- @ControllerAdvice (예외 처리 관련)
- Spring MVC 관련 설정 (예: @RequestMapping, @GetMapping)
- MockMvc 자동 설정
❌ 로딩되지 않는 컴포넌트
- @Service, @Repository, @Component
- Database, JPA 관련 빈
- Security, Scheduling 등 부가적인 기능
→ 그래서 Controller가 사용하는 의존성은 @MockBean 으로 주입해줘야 해요!
@MockBean
private JoinService joinService;
💡 왜 쓰는가?
목적 | 설명 |
빠른 테스트 | 전체 애플리케이션 로딩 없이 Controller만 테스트 |
정확한 HTTP 테스트 | MockMvc로 실제 요청 흐름처럼 테스트 가능 |
Spring REST Docs 연동 | 문서화 자동화 가능 (요청/응답 기반 캡처) |
✅ 실제 적용 코드
@WebMvcTest(JoinController.class)
@DisplayName("유저 회원가입 Controller")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@AutoConfigureMockMvc(addFilters = false) // ✅ 이 부분을 넣어야 Spring Security Filter 작동 안함
class JoinControllerTest {
@MockitoBean
private JoinService joinService;
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
private Long id = 1L;
private String email = "email@email.com";
private String username = "email";
private String password = "password";
private String name = "name";
@Nested
class 회원가입_성공 {
@Test
void 회원가입_성공_정상_회원가입() throws Exception {
// given
JoinRequest request = new JoinRequest(email, password);
JoinResponse response = JoinResponse.builder()
.id(id)
.username(username)
.build();
given(joinService.join(any(JoinRequest.class))).willReturn(response);
// when & then
mockMvc.perform(post("/api/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andDo(print()) // 👈 응답 JSON 전체 콘솔 출력
.andExpect(jsonPath("$.id").value(id))
.andExpect(jsonPath("$.username").value(username));
}
}
@Nested
class 회원가입_실패 {
@Test
void 회원가입_실패_중복된_이메일() throws Exception {
// given
JoinRequest request = new JoinRequest(email, password);
given(joinService.join(any(JoinRequest.class)))
.willThrow(new DuplicateUserException());
// when & then
mockMvc.perform(post("/api/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.message").value(UserError.DUPLICATE_USER.getMessage()))
.andExpect(jsonPath("$.code").value(UserError.DUPLICATE_USER.name()))
.andExpect(jsonPath("$.status").value(UserError.DUPLICATE_USER.getStatus().value()))
.andExpect(jsonPath("$.timestamp").exists())
.andExpect(jsonPath("$.details").isMap());
}
}
}
🔍 주의할 점
JoinRequest request = new JoinRequest("test@email.com", "password");
given(joinService.join(request)).willReturn(response);
이런식으로 작성하면 테스트가 실행되면서 mockMvc.perform(...)를 통해 들어온 JoinRequest는
new JoinRequest(...)로 만들어진 다른 인스턴스예요.
즉, Mockito는 내부적으로 아래처럼 비교합니다:
request.equals(실제_호출된_JoinRequest)
그런데 JoinRequest가 equals()를 오버라이딩 하지 않았다면?
👉 Object.equals()는 참조 동일성만 비교해서 false가 됩니다.
사용 방식 | 비교 기준 | 특징 |
any(...) | 타입만 일치하면 됨 | any()는 빠르고 편리하지만 정밀 매칭은 안 됨 |
정확한 객체 (ex. join(request)) | equals() 메서드로 비교함 | equals() 재정의 OR @EqualsAndHashCode 필요 |
'Project > Boilerplate' 카테고리의 다른 글
Mock 테스트 중 엔티티 id 및 SecurityContextHolder 설정 (0) | 2025.04.02 |
---|---|
WebMvcTest + Spring REST doc + Swagger 적용 ( 2 / 2 - Spring REST doc + Swagger ) (0) | 2025.03.29 |
필터 예외 처리 (0) | 2025.03.17 |
API 예외 처리 (0) | 2025.03.17 |
@PreAuthorize를 활용한 댓글 수정/삭제 권한 관리 및 코드 개선 (0) | 2025.03.10 |