[SpringBootTest] 테스트 코드 작성해보기

ziwww·2024년 4월 18일

개발

목록 보기
11/14

Mock이란

소프트웨어 개발에서 Mock이라는 단어는 "흉내"나 "가짜"를 의미하는 용어로 사용된다.
테스트코드에서 주로 사용되는 개념인데, 원래의 객체나 기능을 흉내내어 테스트 환경을 조장하거나 시뮬레이션하는데 사용된다.

언제 Mock 객체를 만들것인가?

  • 외부 의존성을 가진 코드를 테스트할 때
  • 테스트 시간이 오래 걸릴 때
  • 테스트 작성을 위한 환경 구축이 어려울 때

외부 의존성을 가진 코드를 테스트할 때

Mocking은 외부 환경의 영향을 최소화하여 테스트를 할 수 있는 장점이 있다.
예를들어, 데이터베이스에 접근하는 메소드를 테스트하는 경우, 실제 데이터베이스 연결을 사용하지 않고 Mocking된 데이터베이스 객체를 사용하여 데이터베이스 액세스 동작을 시뮬레이션 할 수 있다.
이렇게하면 데이터 베이스 연결에 의존하지 않고도 해당 함수를 테스트 할 수 있다.

테스트 시간이 오래 걸릴 때

테스트 케이스의 실행 시간 단축을 위해 Mocking을 사용하는 경우가 있따.
특정 모듈을 호출했을 때 네트워크를 타는 경우 시간이 걸리는 경우가 이런 경우이다.
기능 수행 그 자체보다 테스트 수행 시 영향을 미치는 다른 부분으로 인해 테스트 시간이 오래 걸리는 경우 Mock이 사용된다.

테스트 작성을 위한 환경 구축이 어려울 때

아직 개발이 안된 서비스 코드와 연동이 필요할 때, 연계 모듈을 전달받지 못했을 때 Mocking처리를 해서 정상 동작을 가정하고 작성한 기능 동작을 테스트해 볼 수있다.
그 외 경우에도 웹 서버나 웹 어플리케이션 서버, DB 서버 등 설치해야만 테스트 케이스가 가능해지는 경우가 될 수도 있다.

Mockito란?

Mockito는 가짜 객체를 만들어 줄 수 있는 테스트 라이브러리이다. Spring으로 웹 애플리케이션을 개발하다보면 여러 객체간의 의존성을 갖게되는데, 이러한 의존성은 단위 테스트를 작성하기 어렵게 한다.
이를 해결하기 위해 가짜 객체를 주입 시켜주는 Mockito라이브러리를 활용할 수 있다. Mockito를 활용하면 가짜 객체에 원하는 결과를 stub해서 단위 테스트를 진행할 수 있다.

Mockito 사용법

Mock 객체 의존성 주입

Mockito에서 가짜 객체의 의존성을 주입하기 위해서는 다음 3가지 어노테이션을 사용한다.
1. @InjectMocks: @Mock이나 @Spy 어노테이션으로 생성한 가짜 객체를 자동으로 주입시켜주는 어노테이션
2. @Mock: 가짜 객체를 만들어주는 어노테이션
3. @Spy: Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션
*여기서 stub이란, 더비 객체가 마치 실제로 동작하는 것 처럼 보이게 만들어놓은 객체

Stub으로 결과값 처리

의존성이 있는 객체는 가짜 객체를 주입하여, 마치 실제로 동작하는 것 처럼 보이게 만든다. 특정 결과를 반환하라고 정해진 답변을 준비시키는 것. Mockito에서는 다음과 같은 Stub 메소드를 제공한다.

  • doReturn(): 가짜 객체가 특정한 값을 반환해야 하는 경우
  • doNothing(): 가짜 객체가 아무 것도 반환하지 않는 경우(void)
  • doThrow(): 가짜 객체가 예외를 발생시키는 경우

Mockito와 JUnit 함께 사용하기

@ExtendWith(MockitoExtension.class) 어노테이션을 사용해야 결합이 가능


서론이 길었다.. 나는 따로 테스트 db서버를 두지 않았기 때문에 repository는 테스트하지 않고 controller와 service만 테스트 할 것이다.


Service 계층 단위 테스트 작성하기

하나하나 설명하기엔 너무 많으니 한개씩만 하겠다..

@ExtendWith(MockitoExtension.class)
class ImageServiceImplTest {
    @Mock
    private ImageRepository imageRepository;
    @Mock
    private UserRepository userRepository;
    @Mock
    private AmazonS3 amazonS3;
    
    ...
    
    @Test
    @DisplayName("새로운 이미지가 없고 기존 이미지가 수정 됐을 때 수정 테스트")
    void UpdateFIleList_withNoNewImage() {
        //given
        Board board = mock(Board.class);
        //1번,2번,3번 사진에서 1번,3번으로 수정되었다.
        List<String> originalFiles = Arrays.asList("images/test.png", "images/test3.png");

        List<Image> imageList = createImageList();
        when(imageRepository.findAllByBoard_IdOrderByOrder(any())).thenReturn(Optional.of(imageList));

        //when
        imageService.updateFileList(board, originalFiles, null);

        //then
        for (Image image : imageList) {
            if (!originalFiles.contains(image.getName())) {
                //originalFile에 해당 안되는 파일이 1개 지워졌는지 확인
                verify(imageRepository, times(1)).delete(image);
            } else {
                verify(imageRepository, never()).delete(image); // 기존 이미지는 삭제되지 않아야 함
            }
        }

        //순서 바뀌었는 지 확인
        assertThat(imageList.get(0).getOrder()).isEqualTo(1);
        assertThat(imageList.get(2).getOrder()).isEqualTo(2);
    }
  • Board board = mock(Board.class) = Board 객체의 목(mock)을 생성
  • List imageList = createImageList() = 테스트에서 사용할 이미지 목록을 생성하는 메서드를 호출
  • when(imageRepository.findAllByBoard_IdOrderByOrder(any())).thenReturn(Optional.of(imageList)) = imageRepository에서 findAllByBoard_IdOrderByOrder() 메서드가 호출될 때, 아까 만든 이미지 목록을 반환하도록 목 객체에 설정
  • imageService.updateFileList(board, originalFiles, null) = 서비스의 updateFileList() 메서드를 호출
          for (Image image : imageList) {
            if (!originalFiles.contains(image.getName())) {
                //originalFile에 해당 안되는 파일이 1개 지워졌는지 확인
                verify(imageRepository, times(1)).delete(image);
            } else {
                verify(imageRepository, never()).delete(image); // 기존 이미지는 삭제되지 않아야 함
            }
        }

이미지가 삭제되었는지 안되었는지 확인하기 위해 imageRepository의 delete() 메서드가 호출되었는지 검증

        assertThat(imageList.get(0).getOrder()).isEqualTo(1);
        assertThat(imageList.get(2).getOrder()).isEqualTo(2);
  • assertThat(imageList.get(0).getOrder()).isEqualTo(1);: 이미지 목록의 첫 번째 이미지의 순서가 1인지 확인
  • assertThat(imageList.get(2).getOrder()).isEqualTo(2);: 이미지 목록의 세 번째 이미지의 순서가 2인지 확인

Controller계층 단위에서 테스트 하기

👉 @WebMvcTest는 MockMvc를 빈으로 등록하기 때문에,

@WebMvcTest만 선언해줘도, MockMvc 객체가 주입되게 된다.

@WebMvcTest(BoardController.class)
@MockBean(JpaMetamodelMappingContext.class)
class BoardControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BoardService boardService;
    @MockBean
    private CommentService commentService;
    @MockBean
    private CustomIUserDetailsService userDetailsService;
    @MockBean
    private ReportService reportService;
  	
  	...
  
      @DisplayName("저장테스트")
    void saveBoard() throws Exception {
        //given
        BoardCreateRequestDTO requestDTO = createDummyBoardCreateDTO("Test Content", Arrays.asList("tag1", "tag2"));
        List<MultipartFile> images = createDummyImageList();

        // Mocking userDTO
        UserDTO userDTO = new UserDTO();
        userDTO.setIdx(1L); // 가짜 user idx 값 설정
        when(userDetailsService.getCurrentUserDTO()).thenReturn(userDTO);

        //Mocking Board
        Board board = Board.builder()
                .id(1L).build();// 가짜 ID 값 설정
        when(boardService.save(eq(userDTO.getIdx()), any(BoardCreateRequestDTO.class), eq(images))).thenReturn(board);

        //when
        ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.multipart("/board/write")
                .file((MockMultipartFile) images.get(0))
                .file((MockMultipartFile) images.get(1))
                .param("content", requestDTO.getContent())
                .param("tags", requestDTO.getTags().get(0))
                .param("tags", requestDTO.getTags().get(1))
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .accept(MediaType.APPLICATION_JSON_UTF8));

        //then
        resultActions.andExpect(MockMvcResultMatchers.status().isOk());
    }
  • when(userDetailsService.getCurrentUserDTO()).thenReturn(userDTO);: userDetailsService에서 현재 사용자 정보를 가져올 때, 위에서 설정한 더미 사용자 정보를 반환하도록 설정
  • when(boardService.save(eq(userDTO.getIdx()), any(BoardCreateRequestDTO.class), eq(images))).thenReturn(board);: boardService의 save 메서드가 호출될 때, 위에서 설정한 더미 게시물 객체를 반환하도록 설정
        //when
      ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.multipart("/board/write")
              .file((MockMultipartFile) images.get(0))
              .file((MockMultipartFile) images.get(1))
              .param("content", requestDTO.getContent())
              .param("tags", requestDTO.getTags().get(0))
              .param("tags", requestDTO.getTags().get(1))
              .contentType(MediaType.APPLICATION_FORM_URLENCODED)
              .accept(MediaType.APPLICATION_JSON_UTF8));

      //then
      resultActions.andExpect(MockMvcResultMatchers.status().isOk());
  • /board/write 엔드포인트에 멀티파트 요청을 수행
    • 요청의 응답 상태 코드가 200(OK)임을 검증한다. 즉, 요청이 성공적으로 처리되었음을 확인
profile
반갑습니다. 오늘도 즐거운 하루입니다.

0개의 댓글