MockMvc 를 사용해보자

Daniel·2024년 8월 9일
0

Back-End

목록 보기
39/42

들어가며

Spring 환경에서는 개발자가 코드를 검증하는 데 도움이 되는 다양한 테스트 프레임워크와 도구가 있습니다.
JUnit 과 같은 단위 테스트 프레임워크, Mockito 와 같은 모의 프레임워크, Spring TestContext Framework 와 같은 통합 테스트 도구가 포함됩니다.
Spring의 테스트 프레임워크는 단위 테스트뿐만 아니라 전체 Spring 애플리케이션 컨텍스트 테스트, 트랜잭션 유효성 검사 및 종속성 모의와 같은 보다 복잡한 시나리오도 지원하도록 설계되었습니다.

이 포스트에서는 Spring 환경의 부분인 SpringMVC 를 테스트하는 MockMvc 에 대해 알아보고자 합니다.

MockMvc 란?

MockMvcSpring Framework 에서 제공하는 테스트 도구로, Spring MVC 애플리케이션의 웹 계층(주로 컨트롤러)을 테스트하기 위해 사용됩니다.
MockMvc 를 사용하면 실제 HTTP 서버를 실행하지 않고도 Spring MVC 컨트롤러의 동작을 검증할 수 있습니다.
이를 통해 웹 애플리케이션의 요청 처리 로직을 단위 테스트 수준에서 신속하고 효율적으로 테스트할 수 있습니다.

특징

  • 서버를 구동하지 않는 테스트
    MockMvc는 실제 서버를 띄우지 않고, Spring MVC의 내부 메커니즘을 통해 HTTP 요청을 시뮬레이션합니다.
    이를 통해 서버가 필요한 통합 테스트에 비해 훨씬 빠르고 경량화된 테스트를 수행할 수 있습니다.
  • 다양한 HTTP 요청 시뮬레이션
    GET, POST, PUT, DELETE 등의 다양한 HTTP 요청을 시뮬레이션할 수 있습니다.
    이를 통해 다양한 요청 방식에 대한 컨트롤러의 응답을 테스트할 수 있습니다.
  • 결과 검증
    MockMvc를 통해 응답 코드, 헤더, 본문 등 다양한 요소를 검증할 수 있습니다. 또한, JSON, XML 등의 복잡한 응답 본문도 쉽게 테스트할 수 있습니다.
  • 상태 및 행동 검증
    요청에 대한 컨트롤러의 처리 결과를 검증할 수 있습니다. 예를 들어, 특정 상태 코드가 반환되는지, 특정 뷰가 반환되는지, 또는 특정 예외가 발생했는지 등을 확인할 수 있습니다.
  • 결합 테스트
    MockMvc는 Spring TestContext Framework와 함께 사용되어, Spring 애플리케이션의 다른 부분과 통합된 상태에서도 컨트롤러를 테스트할 수 있습니다.
    이로써 컨트롤러가 실제 환경에서 어떻게 동작하는지 더 잘 이해할 수 있습니다.

MockMvc 설정 및 사용법

  • 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" 인지 확인합니다.

  • 실행 결과

예제를 통한 MockMvc 다른 메서드 알아보기

파일 업로드 테스트

  • 컨트롤러
@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("파일 업로드 성공"));  
}

JSON 유효성 검사 오류 테스트

  • 컨트롤러
@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("비동기 응답"));  
}

HTTP 헤더 테스트

  • 컨트롤러
@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"));  
}
profile
응애 나 애기 개발자

0개의 댓글