MockMvc란?
MockMvc는 Spring MVC의 HTTP 요청-응답 사이클을 실제 서버 없이 테스트하는 프레임워크다. 내장 톰캣을 띄우지 않고 DispatcherServlet을 직접 호출하므로 밀리초 단위의 빠른 컨트롤러 테스트가 가능하다. Spring Boot 3.x에서는 @WebMvcTest 슬라이스 테스트와 조합해 사용한다.
기본 설정: @WebMvcTest
@WebMvcTest(UserController.class) // 특정 컨트롤러만 로드
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean // Spring Boot 3.4+ (@MockBean deprecated)
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
given(userService.findById(1L))
.willReturn(new UserDto(1L, "alice", "alice@example.com"));
mockMvc.perform(get("/api/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("alice"))
.andExpect(jsonPath("$.email").value("alice@example.com"));
}
}
@WebMvcTest는 MVC 관련 빈(컨트롤러, Filter, Interceptor, ControllerAdvice)만 로드하고, Service/Repository는 로드하지 않는다. Spring Test Slice 계층별 테스트에서 다른 슬라이스 어노테이션과 비교할 수 있다.
요청 빌더 심화
POST with JSON Body
@Test
void shouldCreateUser() throws Exception {
var request = new CreateUserRequest("bob", "bob@example.com");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").isNumber());
}
쿼리 파라미터와 페이징
@Test
void shouldSearchWithPaging() throws Exception {
mockMvc.perform(get("/api/users")
.param("keyword", "alice")
.param("page", "0")
.param("size", "20")
.param("sort", "name,asc"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.totalElements").isNumber());
}
파일 업로드 (Multipart)
@Test
void shouldUploadFile() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "report.csv", "text/csv", "id,namen1,alice".getBytes());
mockMvc.perform(multipart("/api/files/upload")
.file(file)
.param("description", "Monthly report"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.filename").value("report.csv"));
}
응답 검증: jsonPath 심화
mockMvc.perform(get("/api/users"))
// 배열 크기
.andExpect(jsonPath("$.content", hasSize(3)))
// 배열 요소 필드 검증
.andExpect(jsonPath("$.content[0].name").value("alice"))
// 존재 여부
.andExpect(jsonPath("$.content[0].id").exists())
.andExpect(jsonPath("$.content[0].password").doesNotExist())
// 타입 검증
.andExpect(jsonPath("$.content[0].age").isNumber())
// 정규식 매칭
.andExpect(jsonPath("$.content[0].email", matchesPattern(".*@.*\.com")))
// 컬렉션 검증
.andExpect(jsonPath("$.content[*].name", containsInAnyOrder("alice", "bob", "charlie")));
// 응답 본문 전체를 문자열로 검증
.andExpect(content().json("""
{"content": [{"name": "alice"}]}
""", false)); // false = lenient (필드 순서 무시, 추가 필드 허용)
Security 통합 테스트
// spring-security-test 의존성 필요
@WebMvcTest(AdminController.class)
@Import(SecurityConfig.class) // Security 설정 로드
class AdminControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
// 인증 없이 접근 → 401
@Test
void shouldReturn401WhenUnauthenticated() throws Exception {
mockMvc.perform(get("/api/admin/stats"))
.andExpect(status().isUnauthorized());
}
// @WithMockUser로 인증된 사용자 시뮬레이션
@Test
@WithMockUser(roles = "ADMIN")
void shouldReturn200ForAdmin() throws Exception {
mockMvc.perform(get("/api/admin/stats"))
.andExpect(status().isOk());
}
// 권한 부족 → 403
@Test
@WithMockUser(roles = "USER")
void shouldReturn403ForRegularUser() throws Exception {
mockMvc.perform(get("/api/admin/stats"))
.andExpect(status().isForbidden());
}
// JWT 토큰 시뮬레이션
@Test
void shouldAcceptJwtToken() throws Exception {
mockMvc.perform(get("/api/admin/stats")
.with(jwt().authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))))
.andExpect(status().isOk());
}
// CSRF 토큰 포함
@Test
@WithMockUser
void shouldRequireCsrf() throws Exception {
mockMvc.perform(post("/api/users")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isCreated());
}
}
Validation 에러 테스트
@Test
void shouldReturn400ForInvalidRequest() throws Exception {
// @NotBlank name, @Email email 검증 실패
var invalid = new CreateUserRequest("", "not-an-email");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalid)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors", hasSize(2)))
.andExpect(jsonPath("$.errors[*].field",
containsInAnyOrder("name", "email")));
}
// @ControllerAdvice 에러 핸들러 통합 검증
@Test
void shouldReturnProblemDetailForNotFound() throws Exception {
given(userService.findById(999L))
.willThrow(new UserNotFoundException(999L));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.title").value("Not Found"))
.andExpect(jsonPath("$.detail").value("User 999 not found"))
.andExpect(jsonPath("$.type").value("about:blank"));
}
커스텀 ResultMatcher 작성
// 반복되는 검증 패턴을 커스텀 Matcher로 추출
public class ApiResultMatchers {
public static ResultMatcher isPageResponse() {
return result -> {
jsonPath("$.content").isArray().match(result);
jsonPath("$.totalElements").isNumber().match(result);
jsonPath("$.totalPages").isNumber().match(result);
jsonPath("$.number").isNumber().match(result);
};
}
public static ResultMatcher hasFieldError(String field) {
return jsonPath("$.errors[*].field", hasItem(field));
}
}
// 사용
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(isPageResponse());
andDo(print())와 디버깅
// 요청/응답 전체 출력 (디버깅용)
mockMvc.perform(get("/api/users/1"))
.andDo(print()) // 콘솔에 요청 헤더, 응답 본문 등 출력
.andExpect(status().isOk());
// 응답 본문을 변수로 받기
MvcResult result = mockMvc.perform(get("/api/users/1"))
.andReturn();
String body = result.getResponse().getContentAsString();
UserDto user = objectMapper.readValue(body, UserDto.class);
assertThat(user.name()).isEqualTo("alice");
MockMvc vs WebTestClient vs RestAssured
| 비교 항목 | MockMvc | WebTestClient | RestAssured |
|---|---|---|---|
| 서버 필요 | ❌ 불필요 | 선택적 | ✅ 필요 |
| 속도 | 가장 빠름 | 중간 | 느림 (HTTP 통신) |
| WebFlux 지원 | ❌ | ✅ | ✅ |
| 적합 상황 | MVC 단위 테스트 | 리액티브 + MVC | E2E 통합 테스트 |
베스트 프랙티스
- 컨트롤러당 하나의 테스트 클래스 —
@WebMvcTest(XxxController.class)로 대상을 한정하면 컨텍스트 로딩 속도가 빨라진다. - Happy Path + Edge Case 모두 커버 — 정상 응답뿐 아니라 400, 401, 403, 404, 409 등 에러 시나리오도 테스트한다.
@MockitoBean최소화 — 필요한 의존성만 Mock한다. 과도한 Mock은 테스트의 신뢰도를 낮춘다.- JSON 파일로 기대값 관리 — 복잡한 응답은
src/test/resources에 JSON 파일로 분리해content().json(resource)로 비교한다. - Testcontainers와 분리 — MockMvc는 빠른 컨트롤러 테스트에, Testcontainers는 느린 통합 테스트에 사용한다.
정리
MockMvc는 Spring MVC 컨트롤러의 가장 효율적인 테스트 도구다. 서버를 띄우지 않아 빠르고, Security·Validation·Exception Handler를 포함한 전체 MVC 파이프라인을 검증한다. @WebMvcTest + @MockitoBean으로 격리하고, jsonPath로 응답을 정밀 검증하며, Security 테스트 지원(@WithMockUser, jwt())까지 활용하면 컨트롤러 계층의 신뢰성을 확보할 수 있다.