반응형
컨트롤러단 테스트를 할 때 주로 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 요청 테스트라 헷갈렸을 뿐....
- 외부 API와의 통신을 테스트하고 싶을 때:
- @RestClientTest를 사용하여 REST 클라이언트의 요청 및 응답 처리를 검증
- 컨트롤러의 요청 매핑 및 응답을 테스트하고 싶을 때:
- MockMvc를 사용하여 컨트롤러 동작을 검증
- 종단 간 테스트를 수행하고 싶을 때:
- REST 클라이언트와 컨트롤러를 포함한 전체 플로우를 테스트하려면 @SpringBootTest를 사용!
728x90
반응형
'개발 > spring' 카테고리의 다른 글
[jpa] 프로시져와 트랜젝션 (0) | 2024.11.19 |
---|---|
[test] @WebMvcTest 에서 @PostConstruct 처리... (2) | 2024.11.18 |
스프링 빈 주입 시 우선 순위 (0) | 2024.11.12 |
[springboot] Test code: profile and configuration (0) | 2024.09.12 |
[jpa] @Id에 wraper 클래스만 사용해야하나? (0) | 2024.07.03 |