개발/spring

[test] @RestClientTest vs mockMvc

방푸린 2024. 11. 16. 22:42
반응형

컨트롤러단 테스트를 할 때 주로 mockMvc를 사용했었는데, @RestClientTest는 뭐지?

:: @RestClientTest는 REST 클라이언트를, MockMvc는 컨트롤러를 테스트

 

MockMvc

목적

  • Spring MVC의 컨트롤러 레이어를 테스트할 때
    • 컨트롤러 레이어의 요청 매핑, 응답 구조, 상태 코드 등을 검증할 때
    • @WebMvcTest는 애플리케이션 컨텍스트에서 컨트롤러와 관련된 빈만 로드
      • @Controller, @RestController, @ControllerAdvice, @JsonComponent 등
  • 실제 HTTP 서버를 띄우지 않고, HTTP 요청과 응답을 모킹하여 컨트롤러 동작을 검증함
  • 비즈니스 로직이 아닌 HTTP 요청-응답 플로우를 테스트하고자 할 때
    • 서비스 계층(bean)이나 리포지토리(bean)는 로드되지 않으며, 이를 테스트하려면 MockBean으로 대체

주요 특징

  • 테스트 범위: 컨트롤러 레이어에 한정. 서비스나 리포지토리 레이어는 빈으로 로드되지 않으며, 해당 의존성은 목(Mock)으로 주입
  • 컨트롤러의 HTTP 요청 및 응답 동작을 테스트하고, 입력 검증(@Valid), JSON 직렬화/역직렬화, HTTP 상태 코드 등을 확인.
  • 어노테이션 설정: 주로 MockMvc와 함께 사용하며, 필요한 의존성을 명시적으로 목(Mock)으로 처리
@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = new User(id, "Alice");
        return ResponseEntity.ok(user);
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        user.setId(1L); // ID는 생성된 값으로 설정
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}
///
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService; // 서비스 계층은 Mock으로 처리
    
    @Test
    void testGetUserById() throws Exception {
        // MockMvc를 사용해 GET 요청 수행
        mockMvc.perform(get("/users/1"))
               .andExpect(status().isOk()) // HTTP 상태 코드 검증
               .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 응답 Content-Type 검증
               .andExpect(jsonPath("$.id").value(1)) // JSON 필드 검증
               .andExpect(jsonPath("$.name").value("Alice"));
    }
    @Test
    void testCreateUser() throws Exception {
        // 요청 본문
        String userJson = """
            {
                "name": "Alice"
            }
        """;

        // MockMvc를 사용해 POST 요청 수행
        mockMvc.perform(post("/users")
                   .contentType(MediaType.APPLICATION_JSON) // 요청 Content-Type 설정
                   .content(userJson)) // 요청 본문 설정
               .andExpect(status().isCreated()) // HTTP 상태 코드 검증
               .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 응답 Content-Type 검증
               .andExpect(jsonPath("$.id").value(1)) // JSON 필드 검증
               .andExpect(jsonPath("$.name").value("Alice"));
    }

}

예외 처리 테스트 가능(controller advice)

컨트롤러가 존재하지 않는 리소스를 요청할 때 적절한 상태 코드와 메시지를 반환하는지 테스트

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NoSuchElementException.class)
    public ResponseEntity<String> handleNoSuchElementException(NoSuchElementException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }
}
/////
@Test
void testUserNotFound() throws Exception {
    // 존재하지 않는 사용자 요청
    mockMvc.perform(get("/users/999"))
           .andExpect(status().isNotFound()) // HTTP 상태 코드 검증
           .andExpect(content().string("User not found")); // 응답 메시지 검증
}

 

@RestClientTest

목적

  • REST 클라이언트(RestTemplate, WebClient) 가 외부 API와 올바르게 상호작용하는지 테스트하는 데 사용
  • 외부 API와의 통신이 예상대로 작동하는지 검증
  • 실제 HTTP 호출을 하지 않고, MockRestServiceServer를 사용하여 요청과 응답을 모킹

주요 특징

  • REST 클라이언트와 관련된 스프링 컨텍스트만 로드
  • 외부 API와의 상호작용(요청/응답 데이터 처리)을 집중적으로 테스트
  • 클라이언트 요청의 헤더, URL, 페이로드 등을 검증
@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(2000); // 연결 타임아웃 2초
        factory.setReadTimeout(2000);    // 읽기 타임아웃 2초
        return new RestTemplate(factory);
    }
}

@Service
public class UserClient {

    private final RestTemplate restTemplate;

    public UserClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public User getUser(Long userId) {
        String url = "/api/users/" + userId;
        return restTemplate.getForObject(url, User.class);
    }

    public User createUser(User user) {
        String url = "/api/users";
        return restTemplate.postForObject(url, user, User.class);
    }
}
/////
@RestClientTest(UserClient.class)
class UserClientTest {

    @Autowired
    private UserClient userClient;

    @Autowired
    private MockRestServiceServer mockServer;

    @Test
    void testGetUser() {
        // Mock 서버 설정
        mockServer.expect(requestTo("/api/users/1"))
                  .andExpect(method(HttpMethod.GET))
                  .andRespond(withSuccess("""
                      {
                          "id": 1,
                          "name": "Alice"
                      }
                  """, MediaType.APPLICATION_JSON));

        // 클라이언트 호출 및 결과 검증
        User user = userClient.getUser(1L);
        assertEquals(1L, user.getId());
        assertEquals("Alice", user.getName());
    }
    
    @Test
    void testCreateUser() {
        // Mock 서버 설정
        mockServer.expect(requestTo("/api/users"))
                  .andExpect(method(HttpMethod.POST))
                  .andExpect(content().json("""
                      {
                          "name": "Alice"
                      }
                  """))
                  .andRespond(withSuccess("""
                      {
                          "id": 1,
                          "name": "Alice"
                      }
                  """, MediaType.APPLICATION_JSON));

        // 클라이언트 호출
        User newUser = new User(null, "Alice");
        User createdUser = userClient.createUser(newUser);

        // 결과 검증
        assertEquals(1L, createdUser.getId());
        assertEquals("Alice", createdUser.getName());
    }

    @Test
    void testTimeout() {
        // Mock 서버에서 응답 지연 설정 (5초)
        mockServer.expect(requestTo("/api/users/1"))
                  .andExpect(method(HttpMethod.GET))
                  .andRespond(request -> {
                      Thread.sleep(5000); // 응답 지연
                      return withSuccess("""
                          {
                              "id": 1,
                              "name": "Alice"
                          }
                      """, MediaType.APPLICATION_JSON).createResponse(request);
                  });

        // 타임아웃 발생 검증
        assertThrows(ResourceAccessException.class, () -> {
            userClient.getUser(1L);
        });
    }
}

 

  • connectTimeout: 서버와 연결하는 데 걸리는 시간 초과 시 예외 발생.
  • readTimeout: 서버로부터 응답을 읽는 데 걸리는 시간 초과 시 예외 발생.

 

MockRestServiceServer

  • RestTemplate 요청을 mock하기 위한 서버
  • 요청의 URL, 메서드, 본문 등을 검증
  • 응답 데이터를 설정하여 실제 API 호출 없이 테스트를 수행
  • content(): 요청 본문(payload)을 검증합니다.
  • withSuccess(): 요청에 대한 성공 응답을 mock

근데 원래의 나라면 restTemplate을 그냥 @Mock 했을 텐데..

MockRestServiceServer이 더 쓰기 좋은 것 같다. 보통 타임아웃 같은 http기능을 테스트하고 하는 게 더 크다 보니..

 

Spring 클라이언트(RestTemplate)에서 타임아웃이 발생하면, 기본적으로 발생한 SocketTimeoutException이 ResourceAccessException으로 감싸져 전달된다..

 

어떤 상황에서 무엇을 선택할까?

사실 전혀 다른 거다..ㅋㅋ 그냥 api 요청 테스트라 헷갈렸을 뿐....

  1. 외부 API와의 통신을 테스트하고 싶을 때:
    • @RestClientTest를 사용하여 REST 클라이언트의 요청 및 응답 처리를 검증
  2. 컨트롤러의 요청 매핑 및 응답을 테스트하고 싶을 때:
    • MockMvc를 사용하여 컨트롤러 동작을 검증
  3. 종단 간 테스트를 수행하고 싶을 때:
    • REST 클라이언트와 컨트롤러를 포함한 전체 플로우를 테스트하려면 @SpringBootTest를 사용!

 

728x90
반응형