소프트웨어 개발에서 Mock이라는 단어는 "흉내"나 "가짜"를 의미하는 용어로 사용된다.
테스트코드에서 주로 사용되는 개념인데, 원래의 객체나 기능을 흉내내어 테스트 환경을 조장하거나 시뮬레이션하는데 사용된다.
Mocking은 외부 환경의 영향을 최소화하여 테스트를 할 수 있는 장점이 있다.
예를들어, 데이터베이스에 접근하는 메소드를 테스트하는 경우, 실제 데이터베이스 연결을 사용하지 않고 Mocking된 데이터베이스 객체를 사용하여 데이터베이스 액세스 동작을 시뮬레이션 할 수 있다.
이렇게하면 데이터 베이스 연결에 의존하지 않고도 해당 함수를 테스트 할 수 있다.
테스트 케이스의 실행 시간 단축을 위해 Mocking을 사용하는 경우가 있따.
특정 모듈을 호출했을 때 네트워크를 타는 경우 시간이 걸리는 경우가 이런 경우이다.
기능 수행 그 자체보다 테스트 수행 시 영향을 미치는 다른 부분으로 인해 테스트 시간이 오래 걸리는 경우 Mock이 사용된다.
아직 개발이 안된 서비스 코드와 연동이 필요할 때, 연계 모듈을 전달받지 못했을 때 Mocking처리를 해서 정상 동작을 가정하고 작성한 기능 동작을 테스트해 볼 수있다.
그 외 경우에도 웹 서버나 웹 어플리케이션 서버, DB 서버 등 설치해야만 테스트 케이스가 가능해지는 경우가 될 수도 있다.
Mockito는 가짜 객체를 만들어 줄 수 있는 테스트 라이브러리이다. Spring으로 웹 애플리케이션을 개발하다보면 여러 객체간의 의존성을 갖게되는데, 이러한 의존성은 단위 테스트를 작성하기 어렵게 한다.
이를 해결하기 위해 가짜 객체를 주입 시켜주는 Mockito라이브러리를 활용할 수 있다. Mockito를 활용하면 가짜 객체에 원하는 결과를 stub해서 단위 테스트를 진행할 수 있다.
Mockito에서 가짜 객체의 의존성을 주입하기 위해서는 다음 3가지 어노테이션을 사용한다.
1. @InjectMocks: @Mock이나 @Spy 어노테이션으로 생성한 가짜 객체를 자동으로 주입시켜주는 어노테이션
2. @Mock: 가짜 객체를 만들어주는 어노테이션
3. @Spy: Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션
*여기서 stub이란, 더비 객체가 마치 실제로 동작하는 것 처럼 보이게 만들어놓은 객체
의존성이 있는 객체는 가짜 객체를 주입하여, 마치 실제로 동작하는 것 처럼 보이게 만든다. 특정 결과를 반환하라고 정해진 답변을 준비시키는 것. Mockito에서는 다음과 같은 Stub 메소드를 제공한다.
@ExtendWith(MockitoExtension.class) 어노테이션을 사용해야 결합이 가능
서론이 길었다.. 나는 따로 테스트 db서버를 두지 않았기 때문에 repository는 테스트하지 않고 controller와 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인지 확인
👉 @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)임을 검증한다. 즉, 요청이 성공적으로 처리되었음을 확인