Comment 관련 Controller,Service 단의 테스트 케이스를 만들면서 기존에 JUnit5 와 Postman 만 다뤘던 것과 달리 mockito, webMvc 를 이용해 자동화 테스트를 만들어 보면서 아주 간단하게 정리 해보았다.
참고 :
https://adjh54.tistory.com/346,
https://velog.io/@myspy/mock%EC%9D%84-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B3%A0-%EC%9E%88%EC%97%88%EC%A7%80mockito
단위 테스트를 위한 Java Mocking Framework
실제 객체의 동작을 모방하는 모의 객체 (Mock Object)를 생성해 코드의 '특정 부분을 격리' 시키고 테스트 하기 쉽게 만들어 준다.
Junit에서 가짜 객체인 Mock 객체를 생성 해주고 관리하고 검증할 수 있도록 지원 해주는 FrameWork
구현체가 아직 없는 경우, 구현체가 있더라도 특정 단위 테스트만 하고 싶은 경우 사용할 수 있는 환경을 제공 해준다.
스프링 부트 2.2 버전 이상의 프로젝트 생성시 자동으로 의존성 추가가 된다.
Mock 객체란?
MockMvc란?
하단의 그림은 Mockito를 사용하지 않고 JUnit5 만 사용해 서비스를 테스트한 테스트 케이스의 흐름이다.
서비스 호출을 위해 매번 LocalServer 를 실행시켜 DB 데이터를 조회 해온다.
이런 불편함을 해결하기 위해 Mockito를 사용한다.

하단 그림은 Mockito를 사용해 서비스를 테스트한 테스트 케이스의 흐름을 보여준다.
Junit5 만 이용했던 방식과 다르게 직접적인 DB 호출 없이 MockObject 라는 모의 객체를 구성해 테스트를 진행한다.
Mock
- Mockito를 사용해 테스트에 필요한 '모의 가짜 리스트' 를 생성한다
Stub
-모의 객체의 메서드 호출에 대한 예상 동작을 정의한다.
Verify- 모의 객체에 대해 특정 메서드가 호출되고 예상된 인자와 함께 호출 되었는지 검증하는 메서드 제공
msv..
mockito 에서 제공하는 주요 메서드
| 메서드 | 설명 |
| mock(Class class) | - 주어진 클래스의 모의 객체를 생성합니다. |
| doReturn(T value) | - 모의 객체의 특정 메서드 호출에 대한 반환 값을 정의합니다. |
| when(T methodCall) | - 주어진 메서드 호출에 대한 스텁(stub)을 정의하여 예상동작을 정의하거나 검증할 수 있도록 합니다. |
| thenReturn(T value) | - when() 메서드와 함께 사용하여 특정 메서드 호출에 대한 반환 값을 지정합니다. |
| given(T methodCall) | - 모의 객체의 메서드 호출 동작을 정의합니다. when() 메서드와 동일한 역할을 합니다 |
| verify(T mock) | - 주어진 모의 객체에 대한 메서드 호출 검증을 수행합니다. |
| any(Class clazz) | - 주어진 클래스에 대해 임의의 인스턴스를 나타내는 Matcher를 생성합니다. |
| eq(T value) | - 주어진 값을 기준으로 매처(matcher)를 생성합니다. - value에는 null이나 원시 타입의 값 또는 객체가 포함될 수 있습니다. |
| verifyNoMoreInteractions(T... mocks) | - 주어진 모의 객체들에 대한 추가적인 상호작용이 없는지 검증합니다. - mocks에는 검증할 모의 객체들의 목록을 전달합니다. |
Controller Test
댓글 생성 테스트
@WebMvcTest(CommentController.class)
@MockBean(JpaMetamodel.class)
//JpaMetamodel : JPA 엔티티의 메타모델을 사용할 수 있게 해주는 클래스
// 메타모델 : 엔티티 클래스의 정보를 담고 있는 클래스
public class CommentControllerTest {
@MockBean
CommentService commentService;
@Autowired
MockMvc mvc;
@Autowired
ObjectMapper mapper;
@Test
@DisplayName("댓글 생성 컨트롤러 테스트")
@WithMockUser //JWTFilter 를 제외시키기 위한 MockUser void createSnsCommentTest() throws Exception{
//given
CommentRequestDto commentRequestDto = CommentRequestDto.builder()
.postId(1L)
.content("댓글 작성 테스트")
.createdBy("Lee")
.build();
String stringJson = createStringJson(commentRequestDto);
//mapper를 통해 Json 으로 변환
CommentResponseDto snsCommentResponseDto = CommentResponseDto.builder()
.id(1L)
.content("댓글 작성 테스트")
.createdBy("Lee")
.build();
given(commentService.createComment(commentRequestDto))
.willReturn(snsCommentResponseDto);
String expectedJson = createStringJson(snsCommentResponseDto);
//then
mvc.perform(post("/api/comment")
.contentType(MediaType.APPLICATION_JSON)
//컨텐츠 타입 JSON 설정 부분
.content(stringJson).with(csrf()))
// csrf 토큰 임의 설정 해주는 부분
.andExpect(status().isOk()) // 상태코드
.andExpect(content().json(expectedJson))
// 컨텐츠 내용이 expectedJson 인지 확인
.andDo(print());
}
public String createStringJson(Object dto) throws JsonProcessingException {
return mapper.writeValueAsString(dto);
}
given().willReturn(); Mockito 메서드를 통해 특정 값을 반환하도록 호출
이후 mvc 객체를 이용해 Post 요청을 보내고 상태 코드와 응답 내용을 검증하고 결과를 출력한다.
댓글 조회 테스트
부모 댓글, 자식 댓글 모두 조회되는지 길이를 확인
@Test @DisplayName("댓글 조회 컨트롤러 테스트") @WithMockUser void getCommentTest() throws Exception {
CommentResponseDto parentComment = CommentResponseDto.builder()
.id(1L)
.postId(1L)
.content("댓글 작성 테스트")
.createdBy("Lee")
.build();
CommentResponseDto childComment = CommentResponseDto.builder()
.id(2L)
.postId(1L)
.content("대댓글 작성 테스트2")
.createdBy("Lee")
.build();
List<CommentResponseDto> commentList = new ArrayList<>();
commentList.add(parentComment);
commentList.add(childComment);
String expectedJson= createStringJson(commentList);
// CommentResponseDto 를 List 로 생성후
// children 과 parent를 add 후 Json 으로 변환하여
// mockhito 를 사용해 특정 서비스 메서드가 호출될 때 값을 반환하도록 한다.
given(commentService.findCommentListByPostId(1L))
.willReturn(commentList);
//movcMvc perform 을 이용해 해당 get API 를 호출하고 postId 부분에는 1L을 삽입 해준다
mvc.perform(get("/api/comment/{postId}", 1L)
.with(csrf()))
.andExpect(content().json(expectedJson))
.andExpect(status().isOk())
.andDo(print());
// content 내용에는 위에서 임의로 만든 expectedJson과 상태 코드 ok를 검증한다.
}
**댓글 생성 예외 테스트**
```java
@Test
@DisplayName("댓글 생성 컨트롤러 예외 테스트 - 댓글 작성시 해당 게시글이 없을 경우")
@WithMockUser
void createCommentExceptionTest() throws Exception{
//given
CommentRequestDto commentRequestDto = CommentRequestDto.builder()
.postId(1L)
.content("댓글 작성 테스트")
.createdBy("Lee")
.build();
String stringJson = createStringJson(commentRequestDto);
given(commentService.createComment(any(CommentRequestDto.class)))
.willThrow(newCustomException(ErrorCode.POST_NOT_FOUND));
// 특정 서비스가 호출 되면 임의로 작성한 예외호출을 하도록 설정 한다.
mvc.perform(post("/api/comment")
.contentType(MediaType.APPLICATION_JSON)
.content(stringJson)
.with(csrf())) .andExpect(status().is(ErrorCode.POST_NOT_FOUND.getHttpStatus().value())).andExpect(jsonPath("$.message").value(ErrorCode.POST_NOT_FOUND.getDetail()))
.andDo(print());
// 예상되는 예외처리와 에러코드 메세지
}
@Test
@DisplayName("댓글 삭제 컨트롤러 테스트")
@WithMockUser
void deleteCommentTest()throws Exception {
mvc.perform(delete("/api/comment/{commentId}", 1L)
.with(csrf())) // CSRF 토큰을 포함합니다.
.andExpect(status().isOk())
.andDo(print());
}
}
@ExtendWith(MockitoExtension.class)
public class CommentServiceTest {
@Mock
CommentRepository commentRepository;
@Mock // Mock 객체 생성
PostRepository postRepository;
@InjectMocks // @Mock으로 등록된 객체를 주입받는다.
CommentService commentService;
@Test
@DisplayName("댓글 작성 단위 테스트")
void createCommentTest() {
//given
CommentRequestDto commentRequestDto = CommentRequestDto.builder().
postId(1L)
.content("댓글 작성 테스트")
.createdBy("작성자")
.build();
CommentResponseDto commentResponseDto = CommentResponseDto.builder()
.id(1L)
.content("댓글 작성 테스트")
.createdBy("작성자")
.build();
Post post = Post.builder().
id(1L)
.title("제목")
.content("댓글 작성 테스트")
.build();
//when
when(postRepository.findById(1L))
.thenReturn(Optional.ofNullable(post));
CommentResponseDto expectedDto = commentService.createComment(commentRequestDto);
//then
Assertions.assertThat(
expectedDto.getCreatedBy()).isEqualTo("작성자");
verify(commentRepository, times(1)).save(any());
// save 메서드가 1번 호출되었는지 검증
// save 메서드는 service.createComment 에서 호출됨
}
@Test
@DisplayName("댓글 작성 예외 테스트 - 게시글이 없을 경우")
void createCommentExceptionTest() {
//given
CommentRequestDto commentRequestDto = CommentRequestDto.builder()
.postId(1L)
.content("댓글 작성 테스트")
.createdBy("작성자")
.build();
when(postRepository.findById(1L)).thenReturn(Optional.empty());
// then
assertThrows(CustomException.class, () -> {
commentService.createComment(commentRequestDto);
}, "해당 게시글 정보를 찾을 수 없습니다.");
}
@Test
@DisplayName("댓글 조회 단위 테스트- 중첩 구조 변환 확인")
void findCommentListByPostId() {
Post post = Post.builder().
id(1L)
.title("제목")
.user(User.builder().email("작성자").build())
.content("댓글 작성 테스트 ")
.build();
Comment parentComment = Comment.builder()
.content("댓글 작성 테스트")
.createdBy("작성자")
.post(post)
.build();
Comment childComment= Comment.builder()
.content("댓글 작성 테스트2222")
.createdBy("작성자")
.post(post)
.parent(parentComment)
.build();
List<Comment> commentList = new ArrayList<>();
commentList.add(parentComment);
commentList.add(childComment);
when(commentRepository.findCommentByPostId(post.getId()))
.thenReturn(commentList);
List<CommentResponseDto> result = commentService.findCommentListByPostId(post.getId());
Assertions.assertThat(result.size()).isEqualTo(2);
verify(commentRepository).findCommentByPostId(post.getId());
}
@Test
@DisplayName("댓글 삭제 테스트")
void deleteCommentTest() {
//given
Long commentId = 1L;
//when
commentService.deleteComment(commentId);
//then
verify(commentRepository, times(1)).deleteById(1L);
}
}