Mockito + Junit5 자동 단위 테스트 작성하기

조대훈·2024년 8월 6일
post-thumbnail

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%80

mockito

단위 테스트를 위한 Java Mocking Framework
실제 객체의 동작을 모방하는 모의 객체 (Mock Object)를 생성해 코드의 '특정 부분을 격리' 시키고 테스트 하기 쉽게 만들어 준다.
Junit에서 가짜 객체인 Mock 객체를 생성 해주고 관리하고 검증할 수 있도록 지원 해주는 FrameWork
구현체가 아직 없는 경우, 구현체가 있더라도 특정 단위 테스트만 하고 싶은 경우 사용할 수 있는 환경을 제공 해준다.
스프링 부트 2.2 버전 이상의 프로젝트 생성시 자동으로 의존성 추가가 된다.

Mock 객체란?

  • 프로그램 개발 테스트를 수행할 모듈과 연결되는 외부의 다른 모듈을 흉내 내는 가자 모듈을 생성해 테스트의 효율성을 높이는 데 사용하는 객체.

MockMvc란?

  • 실제 서버를 구동하지 않고도 HTTP 요청과 응답을 테스트 하는 도구

1. Junit5 의 흐름

하단의 그림은 Mockito를 사용하지 않고 JUnit5 만 사용해 서비스를 테스트한 테스트 케이스의 흐름이다.
서비스 호출을 위해 매번 LocalServer 를 실행시켜 DB 데이터를 조회 해온다.
이런 불편함을 해결하기 위해 Mockito를 사용한다.

2. Junit + Mockito 의 흐름

하단 그림은 Mockito를 사용해 서비스를 테스트한 테스트 케이스의 흐름을 보여준다.
Junit5 만 이용했던 방식과 다르게 직접적인 DB 호출 없이 MockObject 라는 모의 객체를 구성해 테스트를 진행한다.

3. Mockito 수행 과정

  1. 모의 객체 생성 (Mock)
  2. 메서드 호출 예쌍 동작 성정 (Stub)
  3. 메서드 호출 검증 (Verify)

Mock

  • Mockito를 사용해 테스트에 필요한 '모의 가짜 리스트' 를 생성한다
    Stub
    -모의 객체의 메서드 호출에 대한 예상 동작을 정의한다.
    Verify
  • 모의 객체에 대해 특정 메서드가 호출되고 예상된 인자와 함께 호출 되었는지 검증하는 메서드 제공

msv..

4. Mockito 메서드

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 만 테스트 범위에 포함되며 다른 서비스나 리포지 토리는 자동으로 로드 되지 않는다.
    - @McokBean 애너테이션으로 mock 객체를 생성해 주입한다.
  • @MobckBean
    - JPA 메타모델클래스를 mock 으로 생성해 테스트에서 엔티티 메타모델 관련 오류를 발생하지 않도록 하는 애너테이션이다.
  • @WithMockUser
    - 본 테스트를 진행한 프로젝트의 경우에 JWTFilter 에서 인증된 임시 User 를 주입하기 위해 쓰였다.
  • ObjectMapper
    - objectMapper 는 java 객체를 JSON 으로 변환 하거나 반대로 변환하는데 사용된다
  • @MockMvc
    - 실제 서버를 구동하지 않고도 MVC 테스트를 할수 있는 도구

댓글 생성 테스트

@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)
    - JUnit5 에서 Mockito를 사용하기 위해 필요한 애너테이션.
  • @Mock
    - mock 객체를 생성, 실제 객체를 생성하진 않고
  • @InjectMock
    - mock 으로 생성된 객체들을 주입 받을 클래스에 사용한다. 테스트 대상의 인스턴스가 생성되고 해당 클래스의 의존성으로 필요한 Mock 객체들이 자동으로 주입된다.
@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);  
        
    }  
}
profile
백엔드 개발자를 꿈꾸고 있습니다.

0개의 댓글