Spring 환경에서는 개발자가 코드를 검증하는 데 도움이 되는 다양한 테스트 프레임워크와 도구가 있습니다.
JUnit
과 같은 단위 테스트 프레임워크, Mockito
와 같은 모의 프레임워크, Spring TestContext Framework
와 같은 통합 테스트 도구가 포함됩니다.
Spring의 테스트 프레임워크는 단위 테스트뿐만 아니라 전체 Spring 애플리케이션 컨텍스트 테스트, 트랜잭션 유효성 검사 및 종속성 모의와 같은 보다 복잡한 시나리오도 지원하도록 설계되었습니다.
이 포스트에서는 Spring 환경의 부분인 SpringMVC 를 테스트하는 MockMvc
에 대해 알아보고자 합니다.
MockMvc
는 Spring Framework
에서 제공하는 테스트 도구로, Spring MVC
애플리케이션의 웹 계층(주로 컨트롤러)을 테스트하기 위해 사용됩니다.
MockMvc
를 사용하면 실제 HTTP 서버를 실행하지 않고도 Spring MVC 컨트롤러의 동작을 검증할 수 있습니다.
이를 통해 웹 애플리케이션의 요청 처리 로직을 단위 테스트 수준에서 신속하고 효율적으로 테스트할 수 있습니다.
GET
, POST
, PUT
, DELETE
등의 다양한 HTTP 요청을 시뮬레이션할 수 있습니다.MockMvc
클래스를 임포트해서 사용한다.import org.springframework.test.web.servlet.MockMvc;
@RestController
public class ExampleController {
@GetMapping("/endpoint")
public ResponseEntity<String> exampleEndPoint(){
return ResponseEntity.ok("Hello, JAVA");
}
}
// 테스트 범위 : 컨트롤러 로직과 즉각적인 종속성에 중점
// 종속성 : 일반적으로 @MockBean 을 사용하여 종속성 로드
// @ExtendWith(SpringExtension.class) // JUnit 5에서 Spring 통합 테스트를 가능하게 함
// @WebMvcTest(ExampleControllerTest.class) // 웹 계층(컨트롤러) 을 테스트하는 데 사용(전제 Spring 애플리케이션을 로드하지 않음)
// 테스트 범위 : 서비스, 리포지토리 등을 포함한 통합 테스트
// 종속성 : 실제 종속성이 로드
@SpringBootTest // 전체 Spring 애플리케이션을 로드함
@AutoConfigureMockMvc // 테스트에 사용할 MockMvc 를 구성함
class ExampleControllerTest {
@Autowired
protected MockMvc mockMvc;
@Test
void exampleEndPoint() throws Exception {
mockMvc.perform(get("/endpoint"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, JAVA"));
}
}
mockMvc.perform(get("/endpoint"))
: /endpoint 에 대한 GET 요청을 시뮬레이션합니다.
andExpect(status().isOk())
: 응답 상태 코드가 200(OK) 인지 확인합니다.
andExpect(content().string("Hello, JAVA"))
: 응답 본문이 "Hello, JAVA" 인지 확인합니다.
@PostMapping("/upload")
public ResponseEntity<String> fileUpload(@RequestParam("file")MultipartFile file){
if (!file.isEmpty()) {
return ResponseEntity.ok("파일 업로드 성공");
} else {
return ResponseEntity.badRequest().body("파일 업로드 실패");
}
}
@Test
void fileUpload() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"test.txt",
"text/plain",
"Hello, JAVA".getBytes()
);
mockMvc.perform(MockMvcRequestBuilders.multipart("/upload").file(file))
.andExpect(status().isOk())
.andExpect(content().string("파일 업로드 성공"));
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> request) {
String username = request.get("userName");
String password = request.get("password");
Map<String, String> errors = new HashMap<>();
if ((username == null || username.isEmpty()) && (password == null || password.isEmpty())) {
errors.put("error", "ID와 패스워드가 없습니다.");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
if (username == null || username.isEmpty()) {
errors.put("error", "ID가 없습니다.");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
if (password == null || password.isEmpty()) {
errors.put("error", "패스워드가 없습니다.");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
return ResponseEntity.ok(Map.of(
"message", "로그인 성공",
"username", request.get("username")
));
}
@Test
void login() throws Exception {
String userInfo = "{\"username\": \"myID\", \"password\": \"myPASSWORD\"}";
mockMvc.perform(post("/login")
.contentType("application/json")
.content(userInfo))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("로그인 성공"))
.andExpect(jsonPath("$.username").value("myID"));
}
@Test
void login() throws Exception {
String userInfo = "{\"username\": \"\", \"password\": \"\"}";
mockMvc.perform(post("/login")
.contentType("application/json")
.content(userInfo))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value("ID와 패스워드가 없습니다."));
}
@RestController
// Model 객체에 저장되는 데이터를 HttpSession에 저장시키는 어노테이션
// 해당 컨트롤러 안에서 user 라는 이름으로 Model 에 담기는 게 있으면, Session 의 속성으로 옮겨 담는다.
@SessionAttributes("user")
public class ExampleController {
@PostMapping("/session")
public ResponseEntity<String> session(@RequestParam String username, @RequestParam String password, Model model) {
if ("user".equals(username) && "password".equals(password)) {
model.addAttribute("user", username);
return ResponseEntity.ok("로그인 성공");
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("잘못된 자격 증명");
}
@GetMapping("/profile")
public ResponseEntity<String> profile(@SessionAttribute("user") String username) {
return ResponseEntity.ok("Welcome, " + username + "!");
}
}
@Test
@DisplayName("세션에 사용자 속성을 저장한다.")
@Tag("session")
void session() throws Exception {
MockHttpSession session = new MockHttpSession();
MvcResult result = mockMvc.perform(post("/session")
.param("username", "user")
.param("password", "password")
.session(session)
)
.andExpect(status().isOk())
.andExpect(content().string("로그인 성공"))
.andReturn(); // 추가 검사를 위해 MvcResult 반환
// 반환된 result 에서 session 추출
HttpSession httpSession = result.getRequest().getSession(false); // session 을 가져오되, 존재하지 않는 경우 생성하지 않음
// session 은 널인지? 아닌지?
assertThat(httpSession).isNotNull();
// session 의 "user" 속성이 잘 설정 되었는지?
assertThat(Objects.requireNonNull(httpSession).getAttribute("user")).isEqualTo("user");
}
@Test
@DisplayName("세션에 저장된 사용자 속성을 확인한다.")
@Tag("session")
void profile() throws Exception {
MockHttpSession session = new MockHttpSession();
session.setAttribute("user", "user");
MvcResult result = mockMvc.perform(get("/profile")
.session(session)
)
.andExpect(status().isOk())
.andExpect(content().string("Welcome, user!"))
.andReturn();
}
@GetMapping("/async-endpoint")
@Async // 별도의 스레드에서 메서드를 실행하도록 지시합니다. 메서드를 비동기식으로 만듭니다.
public CompletableFuture<ResponseEntity<String>> asyncEndpoint() {
return CompletableFuture.supplyAsync(() -> { // 작업을 비동기적으로 실행하고 미래 결과를 나타내는 CompletableFuture를 반환할 수 있는 Java 8 기능입니다.
// 데이터베이스 쿼리 또는 외부 서비스 호출과 같은 장기 실행 작업 시뮬레이션
try {
Thread.sleep(3000); // 3초의 지연을 시뮬레이션합니다.
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return ResponseEntity.ok("비동기 응답");
});
}
@Test
void asyncEndpoint() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/async-endpoint"))
.andExpect(request().asyncStarted()) // 비동기 처리가 시작되었는지 확인
.andReturn();
mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(status().isOk())
.andExpect(content().string("비동기 응답"));
}
@GetMapping("/headers")
public ResponseEntity<String> handleHeaders(@RequestHeader("X-Custom-Header") String header) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("X-Response-Header", "응답 헤더");
return ResponseEntity.ok().headers(responseHeaders).body("클라이언트 헤더 : " + header);
}
@Test
void handleHeaders() throws Exception {
mockMvc.perform(get("/headers")
.header("X-Custom-Header", "클라이언트 헤더"))
.andExpect(status().isOk())
.andExpect(header().string("X-Response-Header", "응답 헤더"));
}
@PostMapping("/redirect")
public ResponseEntity<Void> handleRedirect() {
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create("/new-location"))
.build();
}
@Test
void handleRedirect() throws Exception {
mockMvc.perform(post("/redirect"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/new-location"));
}
@GetMapping("/cookie-test")
public ResponseEntity<String> handleCookie(@CookieValue("test-cookie") String cookieValue) {
return ResponseEntity.ok("Cookie Value: " + cookieValue);
}
@Test
void handleCookie() throws Exception {
mockMvc.perform(get("/cookie-test")
.cookie(new Cookie("test-cookie", "cookieValue")))
.andExpect(status().isOk())
.andExpect(content().string("Cookie Value: cookieValue"));
}