본문 바로가기
테스트

(나만을 위한)댓글 생성 메소드 테스트 구현 고민과정

by 순원이 2024. 4. 7.

 

    @Transactional
    public Long createPostComment(Long postId, Long userId, PostCommentRequest createDTO) {
        PostComment postComment = buildPostComment(userId, postId, createDTO);

        if (createDTO.getParentCommentId() != null) { // 대댓글일시
            PostComment parentComment = postCommentRepository.findById(createDTO.getParentCommentId())
                    .orElseThrow(() -> new CustomException(HttpStatus.NOT_FOUND, ResultCode.POST_COMMENT_NOT_FOUND));
            parentComment.addCommentReply(postComment);
        }
        PostComment savedComment = postCommentRepository.save(postComment);
        return savedComment.getId();
    }

 

위 Service 메소드의 역할

  • postComment 엔티티 생성
  • 댓글 대댓글 구분 후 대댓글일시 부모댓글에 자식 댓글 추가
  • 댓글 저장

 

단위테스트를 작성하고 싶습니다. createPostCommet 메소드에서 검증해야 할 것이 어떤게 있을까요?

  • postComment엔티티를 잘 생성하는지?
  • 대글 대댓글 구분 후 대댓글일시 부모댓글에 자식 댓글을 잘 추가하는지?
  • 댓글을 잘 저장하는지?
  • request의 parentComment가 null일시 if문 안에 로직이 잘 작동하는지?

그런데 위 세가지는 너무 간단해서 로직이 생각한대로 동작할 것 같아 테스트를 해야하는지 의문입니다. 그래서 저는 return 값이 잘 나오는지 확인하기 보다 save가 제대로 호출됐는지 검증하려고 합니다.(createPostComment의 테스트 코드의 의무가 이것을 검증하는게 맞는지 의문입니다...) 

save가 잘호출됐는지 확인하기 위해 BDD로 작성하였습니다.

public class PostCommentWriteServiceTest {

    @Mock
    private PostCommentRepository postCommentRepository;
    @Mock
    private UserService userService;
    @InjectMocks
    private PostCommentWriteService postCommentWriteService;

    @Test
    @DisplayName("댓글 생성 성공")
    void test1() {
        // Given
        Long postId = 1L;
        Long userId = 1L;
        PostCommentRequest createDTO = new PostCommentRequest();
        createDTO.setContents("test contents");
        createDTO.setParentCommentId(null);

        PostComment mockPostComment = new PostComment();
        given(userService.getUser(anyLong())).willReturn(new User());
        given(postCommentRepository.save(any(PostComment.class))).willReturn(mockPostComment);

        // When
        Long result = postCommentWriteService.createPostComment(postId, userId, createDTO);

        // Then
        then(postCommentRepository).should(never()).findById(anyLong());
        then(postCommentRepository).should(times(1)).save(any(PostComment.class));
        assertThat(result).isNotNull();
    }
}

 

 

'User()' has protected access in 'com.tripmate.tripmate.user.domain.User'

 given(userService.getUser(anyLong())).willReturn(new User()); 부분에서
에러가 발생했습니다.

 

 

에러 이유는 위 부분 때문입니다. 

access = AccessLevel.PROTECTED를 쓴 이유가 궁금하다면 이전 글을 확인해보면 됩니다.

 

@NoArgsConstructor(access = AccessLevel.PROTECTED) 쓰는 이유

@Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) public class PostComment extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") pr

jsw5913.tistory.com

 

 

  • access = AccessLevel.PROTECTED를 Public으로 바꿀까?
  • 아님 모의객체를 사용할까

access = AccessLevel.PROTECTED쓴 이유는 명확하기 때문에 Public으로 바꾸지 않을 것입니다. 그럼 모의객체를 사용해서 해결하겠습니다.

 

수정 코드

 @Test
 @DisplayName("댓글 생성 성공")
	void test1() {
      // Given
      Long postId = 1L;
      Long userId = 1L;
      PostCommentRequest createDTO = new PostCommentRequest();
      createDTO.setContents("test contents");
      createDTO.setParentCommentId(null); 
      
      given(userService.getUser(anyLong())).willReturn(mock(User.class));
      given(postRepository.findById(anyLong())).willReturn(Optional.of(mock(Post.class)));
      given(postCommentRepository.save(any(PostComment.class))).willReturn(mock(PostComment.class));

      // When
      Long result = postCommentWriteService.createPostComment(postId, userId, createDTO);

      // Then
      then(postCommentRepository).should(never()).findById(anyLong());
      then(postCommentRepository).should(times(1)).save(any(PostComment.class));
   }

    @Test
    @DisplayName("request Id값이 1일때 대댓글 생성 성공")
    void test2() {

        // Given
        Long postId = 2L;
        Long userId = 1L;
        PostCommentRequest createDTO = new PostCommentRequest();
        createDTO.setContents("test contents");
        createDTO.setParentCommentId(1L);

        given(userService.getUser(anyLong())).willReturn(mock(User.class));
        given(postRepository.findById(anyLong())).willReturn(Optional.of(mock(Post.class)));
        given(postCommentRepository.findById(anyLong())).willReturn(Optional.of(mock(PostComment.class)));
        given(postCommentRepository.save(any(PostComment.class))).willReturn(mock(PostComment.class));

        // When
        Long result = postCommentWriteService.createPostComment(postId, userId, createDTO);

        // Then
        then(postCommentRepository).should(times(1)).findById(anyLong());
        then(postCommentRepository).should(times(1)).save(any(PostComment.class));
    }

 

코드를 수정하고 테스트가 통과됐지만, 찝찝합니다. 앞에서 언급했던 createPostCommetn메소드의 단위테스트 역할을 다 했나?가 의문이고 BDD를 이럴 때 쓰는 게 맞나 생각이 듭니다. 

BDD는 사용자 시나리오 검증입니다. 
현재 테스트의 given은 createPostCommet 메소드내에 있는 함수들이 가상의 값을 리턴하도록 설정한 것입니다. 사용자 시나리오가 전혀 아닙니다. 
진짜 BDD는 아래와 같이 사용자 시나리오를 설정하는 거 아닐까요?

 

 

단위테스트를 작성하고 싶어 지금까지 위와 같이 작성했습니다. 위 방법이 틀리다며  mockMVC.perform()을 이용하여 직접 API를 호출하여 통합테스트를 작성해야 될까요?


혹은 미리 관련 엔티티를 세팅하고 createPostComment만 호출한다 해도 User와 Post를 미리 데이터베이스에 저장하고 마지막에 Comment가 잘 저장되었는지 확인하기 위해 데이터베이스를 사용해야 하는데 Service 단위 테스트를 하기위해 Reposiory를 쓰는 건 잘못된 거 아닌가요?