프로젝트를 하면서 단위 테스트를 짜야지 하고 짰던 코드가 단위 테스트가 아니였다는 것을 알았다.
단순히 Controller, Service, Repository 단위로 짜면 되는게 아닌가라고 생각했던 나..
피드백 해주셨던 분이 단위 테스트도 있으면 좋겠다는 말에 내가 짠 게 단위 테스트가 아닌 것을 깨닫고 알아봤다.
@WebMvcTest(BoardController.class)
@MockBean(JpaMetamodelMappingContext.class)
public class BoardControllerTest {
@MockBean
BoardService boardService;
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
BoardRequestDto boardRequestDto;
BoardResponseDto boardResponseDto;
@Test
public void createBoard_Success() throws Exception {
boardRequestDto = BoardRequestDto.builder()
.title("test board title")
.content("test board content")
.build();
boardResponseDto = new BoardResponseDto(1L, boardRequestDto.getTitle()
, boardRequestDto.getContent(), LocalDateTime.now());
given(boardService.createBoard(any(BoardRequestDto.class))).willReturn(boardResponseDto);
mockMvc.perform(post("/board")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(boardRequestDto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("success"))
.andExpect(jsonPath("$.data.id").value(boardResponseDto.getId()))
.andExpect(jsonPath("$.data.title").value(boardResponseDto.getTitle()))
.andExpect(jsonPath("$.data.content").value(boardResponseDto.getContent()))
.andDo(print());
}
위는 기존에 짰던 Controller의 게시물 생성 기능을 테스트하는 함수이다.
필요한 RequestDto 객체를 만들어 Service 함수에 대한 설정을 해주고 MockMvc를 사용하여 어떻게 응답 메세지로 전달이 되는지까지 적혀있다.
사용된 어노테이션을 간단하게 정리해보자면,,
-> 스프링 컨텍스트를 로드하여 실행되면서 스프링의 여러 빈 관리와 MVC 설정이 포함된 것을 확인할 수 있다.
그래서 단위 테스트 보다는 스프링 MVC 계층의 통합 테스트에 더 가까운 코드라고 할 수 있다.
그럼 이제 내가 기존에 짰던 코드를 완전 독립적으로 테스트할 수 있는 코드로 만들어보자.
내가 원하는 단위 테스트는 개별 클래스나 메서드를 외부 의존성 없이 고립시켜 테스트하는 것이다.
Controller, Service, Repository 서로 의존하는 것이 아닌 독립적으로 각자의 클래스가 가진 메서드들이 정상적으로 작동하는지 확인할 수 있는 테스트 코드.
그러려면..
컨트롤러 테스트에는 BoardController와 BoardService가 필요하다.
이때 사용될 수 있는 것으로는 객체를 모킹하는 @Mock과 @InjectMocks가 있다.
@Mock
특정 클래스의 가짜(Mock) 객체를 생성한다.
해당 클래스의 실제 인스턴스를 생성하는 것이 아닌 메서드 호출 시 원하는 동작을 설정할 수 있을 정도의 가짜 객체를 만들어준다.
@InjectMocks
실제 객체를 생성하고 @Mock 객체들을 주입하여 사용할 수 있게 해준다.
테스트하려는 대상의 객체에 필요한 의존성을 자동으로 주입하여 객체를 초기화해준다.
그렇다면 테스트 대상인 BoardController 클래스에는 @InjectMocks 어노테이션을 달아주고 BoardService 객체에는 @Mock 어노테이션을 달아주어 테스트 시 사용하면 된다.
@Mock
BoardService boardService;
@InjectMocks
BoardController boardController;
테스트하려고 하는 BoardController 클래스의 createBoard 함수는 아래와 같다.
@PostMapping
public ResponseDto<BoardResponseDto> createBoard(@Valid @RequestBody BoardRequestDto boardRequestDto) {
BoardResponseDto boardResponseDto = boardService.createBoard(boardRequestDto);
return ResponseDto.success(boardResponseDto);
}
그렇다면 컨트롤러의 createBoard 메서드는 BoardRequestDto 객체를 받고 BoardService의 createBoard 함수에 넘겨주고 받은 BoardResponseDto를 잘 받아서 넘겨주면 된다.
이에 필요한 것은 BoardRquestDto 객체와 가짜로라도 사용될 BoardService의 createBoard 함수일 것이다.
그래서 아래와 같이 BoardControllerTest 클래스에 추가해 주었다.
BoardRequestDto boardRequestDto;
BoardResponseDto boardResponseDto;
@BeforeEach
public void setUp() {
boardResponseDto = new BoardResponseDto(1L, "test title", "test content", LocalDateTime.now());
}
이제 createBoard 메서드를 테스트하는 함수를 짜주자.
@Test
public void createBoard_Success() {
/*given*/
boardRequestDto = new BoardRequestDto("test title", "test content");
when(boardService.createBoard(any(BoardRequestDto.class))).thenReturn(boardResponseDto);
/*when*/
ResponseDto<BoardResponseDto> boardResponse = boardController.createBoard(boardRequestDto);
/*then*/
assertEquals("success", boardResponse.getStatus());
assertEquals(boardRequestDto.getTitle(), boardResponse.getData().getTitle());
assertEquals(boardRequestDto.getContent(), boardResponse.getData().getContent());
}
BoardController 클래스의 createBoard 함수를 테스트하는 함수는 다 짜졌다.
근데 앞에서 우리가 @Mock과 @InjectMocks를 통해서 모의 객체를 사용한다고 하였다.
그러면 이 객체는 어떻게 초기화가 되어 사용될 수 있는 것일까?
그 역할을 해주는 것이 @ExtendWith(MockitoExtension.class) 이다.
@ExtendWith(MockitoExtension.class)
@ExtendWith
Junit5 부터 제공되는 어노테이션으로 Junit4에서 사용되던 @RunWith의 업그레이드 버전이다.
기존 @RunWith는 하나의 Runner만 사용이 가능하여 복잡한 테스트 시나리오를 적용시에 어려움이 있었다. 하지만 Junit5에 @ExtendWith 어노테이션이 나오게 되면서 여러 Extension들을 지정이 가능하게 되면서 유연하고 다양한 기능을 확장할 수 있게 되었다.
내가 개발한 프로젝트는 Junit5이기 때문에 @ExtendWith를 사용할 것이고 그중 Mockito와 관련된 기능을 확장하여 테스트하기 위해선 MockitoExtension.class를 넣어주면 된다.
그래서 다시 어떻게 초기화가 되어서 사용되는 것이냐면,
✔️ @ExtendWith(MockitoExtension.class)를 사용하면 @Mock, @InjectMocks 등과 같이 Mockito 어노테이션이 설정된 필드가 자동으로 초기화된다.
✔️ Mockito의 테스트 생명 주기도 관리해준다.
테스트 실행 후 사용되지 않는 스터빙을 자동으로 검출하고 InnecessaryStubbingException을 발생시켜 불필요한 스터빙을 제거하도록 유도한다.
그래서 BoardControllerTest 클래스에 @ExtendWith(MockitoExtension.class)를 달아줬다.
@ExtendWith(MockitoExtension.class)
public class BoardControllerTest {
❓Junit4에서는 어떻게 Mockito 관련 객체들을 초기화시켜줬을까?
Junit4의 @RunWith에는 MockitoExtension.class를 사용해 확장할 수 없다.
그래서 MockitoAnnotations.openMocks(this) 함수를 사용해줬다.
MockitoAnnotations.openMocks(this); 함수는 Mockito 어노테이션 기반의 Mock 객체들을 초기화해주는 메서드이다.
테스트가 실행됨과 동시에 초기회되어야하니 @BeforeEach 함수에 넣어주면 된다.
@BefordEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
이제 BoardController 테스트 코드는 다 짜졌다.
BoardControllerTest 총 코드
@ExtendWith(MockitoExtension.class)
public class BoardControllerTest {
@Mock
BoardService boardService;
@InjectMocks
BoardController boardController;
BoardRequestDto boardRequestDto;
BoardResponseDto boardResponseDto;
@BeforeEach
public void setUp() {
boardResponseDto = new BoardResponseDto(1L, "test title", "test content", LocalDateTime.now());
}
@Test
public void createBoard_Success() {
boardRequestDto = new BoardRequestDto("test title", "test content");
when(boardService.createBoard(any(BoardRequestDto.class))).thenReturn(boardResponseDto);
ResponseDto<BoardResponseDto> boardResponse = boardController.createBoard(boardRequestDto);
assertEquals("success", boardResponse.getStatus());
assertEquals(boardRequestDto.getTitle(), boardResponse.getData().getTitle());
assertEquals(boardRequestDto.getContent(), boardResponse.getData().getContent());
}
}
테스트 코드를 제대로 짜본 적이 없어 이번 기회에 하나씩 훑어보는 시간을 가졌는데
정말 다양한 테스트 케이스들이 많다보니 아직 지금 짠 코드도 정확하게 짠 것인지는 확신이 서지 않는다.
그래도 이번 기회에 내가 테스트 코드에서 반영하고자 하는 방향대로 짜려고 했던 경험과 공부해가며 얻은 내용들이 정말 많았고 그것만으로도 좋은 경험이 될 것이다.