Response Body가 빈 값일 때
테스트를 아래와 같이 작성했다.
@Test
@WithMockUser
void testGetReplies() throws Exception {
Long postId = 1L;
Pageable pageable = PageRequest.of(1, 10);
User user = User.builder()
.id(1L)
.email("test@test.test")
.name("test")
.authProvider(AuthProvider.local)
.picture(null)
.role(Role.ROLE_USER)
.password("password")
.build();
Post post = Post.builder()
.id(postId)
.title("안녕하세요.")
.content("반갑습니다.")
.user(user)
.build();
List<Reply> list = new ArrayList<>();
list.add(Reply.builder().user(user).post(post).content("댓글").build());
ReplyListResponseDto response = ReplyListResponseDto.builder()
.replies(list)
.pageable(pageable)
.totalCount(1L)
.totalPages(1L)
.build();
given(replyService.findReplyByPostId(postId, pageable))
.willReturn(response);
mockMvc.perform(
get("/reply/" + postId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.replies").exists())
.andDo(print());
verify(replyService).findReplyByPostId(postId, pageable);
}
계속해서 테스트가 실패했다. .andExpect에서 응답은 200으로 통과하는데 replies 값이 없어서 실패하고 있었다.
에러 1
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
No value at JSON path "$.replies"
java.lang.AssertionError: No value at JSON path "$.replies"
...
Caused by: java.lang.IllegalArgumentException: json can not be null or empty
...
콘솔을 보니 Body가 아예 빈 값이었다. 나는 response 값을 직접 만들어서 .willReturn에 잘 넣어줬는데 도대체 왜 이런 일이 발생하는지 알 수가 없었다.
mock 객체의 메서드 호출이 잘 되고 있는지 확인이 필요하다고 했다.
andExpect를 주석처리하고 아래 verify 메서드로 replyService 메서드를 제대로 호출하고 있는지 확인했다.
Argument(s) are different! Wanted:
com.board.service.ReplyService#0 bean.findReplyByPostId(
1L,
Page request [number: 1, size 20, sort: UNSORTED]
);
-> at com.board.controller.ReplyApiControllerTest.testGetReplies(ReplyApiControllerTest.java:87)
Actual invocations have different arguments:
com.board.service.ReplyService#0 bean.findReplyByPostId(
1L,
Page request [number: 0, size 20, sort: UNSORTED]
);
-> at com.board.controller.ReplyApiController.getList(ReplyApiController.java:62)
mock 객체를 설정할 때 파라미터를 잘못 전달했다. 지금 테스트 코드에서 api를 호출할 때 페이지 값을 전달하지 않고 있다. api에서 파라미터에 페이지 값이 없으면 0, 20으로 기본 설정 하고 있다. 내가 임의로 전달한 값과 기본으로 전달될 값이 달라서 에러가 발생.
Pagable 객체를 기본 값으로 수정했더니 테스트에 성공했다.
Pageable pageable = PageRequest.of(0, 20);
403 Forbidden 에러 발생!
테스트 코드
- 테스트한 api는 댓글을 등록하는 post 요청이었고 로그인이 필요한 기능이다.
@Test
@WithMockUser
void TestSaveReply() throws Exception {
ReplySaveRequestDto replyDto = ReplySaveRequestDto.builder()
.content("댓글입니다.")
.postId(1L)
.userId(1L)
.build();
given(replyService.save(replyDto)).willReturn(1L);
String request = new ObjectMapper().writeValueAsString(replyDto);
mockMvc.perform(
post("/reply")
.content(request)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print());
verify(replyService).save(replyDto);
}
에러 2
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Status expected:<200> but was:<403>
Expected :200
Actual :403
java.lang.AssertionError: Status expected:<200> but was:<403>
- 이번에는 다른 게 아니고 403 에러가 발생한 것 보니 token filter만 필터링할 것이 아니고 security 설정도 피하도록 설정을 해야겠다.
verify를 실행하여 확인
Wanted but not invoked:
com.board.service.ReplyService#0 bean.save(
com.board.dto.reply.ReplySaveRequestDto@5065bdac
);
-> at com.root.pilot.board.controller.ReplyApiControllerTest.TestSaveReply(ReplyApiControllerTest.java:112)
Actually, there were zero interactions with this mock.
Wanted but not invoked:
com.board.service.ReplyService#0 bean.save(
com.board.dto.reply.ReplySaveRequestDto@5065bdac
);
-> at com.board.controller.ReplyApiControllerTest.TestSaveReply(ReplyApiControllerTest.java:112)
Actually, there were zero interactions with this mock.
- mock 객체가 해당 메서드를 호출하지 못했다는 의미
- 이미 호출 시점에 권한 에러가 떠서 아예 호출이 이루어지지 않음
보안 필터를 무시하고 테스트를 실행할 수 있는 어노테이션 추가
@AutoConfigureMockMvc(addFilters = false)
- 주의할 점은 인증된 유저를 조회할 때 null로 나오기 때문에 WithMockUser에 정보를 추가해줘야 한다.
@WithMockUser(username = "username", password = "1234")
- 어노테이션에 가짜 유저 정보 작성이 가능하다.
- @WithUserDetail: UserDetail 객체를 넣어서 사용이 가능하다. UserDetailService 함께 등록 필요
다시 돌아온 빈 응답 값
에러 3
Argument(s) are different! Wanted:
com.board.service.ReplyService#0 bean.save(
com.board.dto.reply.ReplySaveRequestDto@346330b6
);
-> at com.board.controller.ReplyApiControllerTest.TestSaveReply(ReplyApiControllerTest.java:115)
Actual invocations have different arguments:
com.board.service.ReplyService#0 bean.save(
com.board.dto.reply.ReplySaveRequestDto@51e0f2eb
);
-> at com.board.controller.ReplyApiController.save(ReplyApiController.java:33)
- 인자로 넣은 객체가 값은 같지만 참조 값이 달라 나는 에러로 보인다.
해결방법
1. 객체에 equals 메서드 구현
2. ArgumentCaptor 사용
3. any() 사용
ArgumentCaptor 사용
테스트를 위해 equals 메서드를 구현하기 보다는 먼저 ArgumentCaptor를 사용해보기로 했다.
ArgumentCaptor<ReplySaveRequestDto> captor = ArgumentCaptor.forClass(ReplySaveRequestDto.class);
given(replyService.save(captor.capture())).willReturn(1L);
...
verify(replyService).save(captor.capture());
- org.mockito.ArgumentCaptor를 사용하여 파라미터를 전달했다.
ArgumentCaptor는 mock 객체를 사용할 때 전달된 인자들을 캡쳐(기록하고 저장)하고 검증하는 데 사용한다. 해당 인자를 검사하고 테스트할 수 있다.
캡쳐한 값 가져오기
ReplySaveRequestDto capturedArgument = captor.getValue();
any() 사용
given(replyService.save(any())).willReturn(1L);
...
verify(replyService).save(any());
any()는 특정 인자에 대한 검증은 필요없고 메서드가 호출 되었는지 여부만 확인할 때 사용한다.
이 테스트에서는 인자와 관계 없이 저장된 댓글의 id만 반환하기 때문에 요청 값과 비교하여 검증할 것이 없다. 그래서 그냥 any()를 사용하기로 했다.
참고
'개발 > Spring' 카테고리의 다른 글
| [batch] spring boot3에서 배치 실행이 안됨 (1) | 2024.02.08 |
|---|---|
| 외부 api 연동하기(retrofit2, form urlencoded) (0) | 2023.12.14 |
| WebMvcTest에서 Filter 제외하기 (0) | 2023.09.08 |
| [SpringBoot] MySQL 연결 에러 (0) | 2023.09.06 |
| [SpringBoot] 이메일 전송(텍스트, html) (0) | 2023.09.04 |