오늘은 지난 시간에 이어서 Controller 테스트 코드를 작성하고, JUnit 연습을 마치려고합니다.
테스트 코드를 공부하고는 도커를 배워보려고합니다.
예전에 도커가 어떤 기술인지 알아보기만하고 방치해놔서...ㅎㅎ;;
밀린 강의 들으면서 개강할 준비 해야겠네요...
일단 시작하기에 앞서, 본코드를 작성해보겠습니다.
테스트 코드를 공부하는 입장이니, 일단은 간단하게 Controller를 작성하였습니다.
import com.example.JUnitTest.dto.BookEditDto;
import com.example.JUnitTest.dto.BookSaveDto;
import com.example.JUnitTest.service.BookService;
import com.example.JUnitTest.web.dto.BookResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class BookController {
private final BookService bookService;
@GetMapping("/books")
public ResponseEntity<List<BookResponseDto>> getBooks() {
return new ResponseEntity<>(bookService.getBooks(), HttpStatus.OK);
}
@GetMapping("/book/{id}")
public ResponseEntity<BookResponseDto> getBook(@PathVariable Long id) {
return new ResponseEntity<>(bookService.getBook(id), HttpStatus.OK);
}
@PostMapping("/books")
public ResponseEntity<Long> saveBook(BookSaveDto saveDto) {
return new ResponseEntity<>(bookService.saveBook(saveDto), HttpStatus.CREATED);
}
@PutMapping("/book/{id}")
public ResponseEntity<Long> editBook(@PathVariable Long id, BookEditDto editDto) {
return new ResponseEntity<>(bookService.editBook(editDto, id), HttpStatus.OK);
}
@DeleteMapping("/book/{id}")
public void deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
}
}
다음처럼 간단한 CRUD 기능을 구현한 Controller 파일을 생성하였습니다.
import com.example.JUnitTest.service.BookService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@ExtendWith(MockitoExtension.class)
public class BookControllerTest {
@Mock
private BookService bookService;
@InjectMocks
private BookController bookController;
private MockMvc mockMvc;
private ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
public void initMockMvc() {
mockMvc = MockMvcBuilders
.standaloneSetup(bookController)
.build();
}
}
다음처럼 Mockito와 MockMvc를 활용하여 테스트 코드를 작성하였습니다.
@Test
@DisplayName("책 전체 조회 테스트")
public void getBooksTest() throws Exception {
//given
List<BookResponseDto> resultList = new ArrayList<>();
resultList.add(new BookResponseDto("Test Title1", "Test Content1", "Test Writer1"));
resultList.add(new BookResponseDto("Test Title2", "Test Content2", "Test Writer2"));
resultList.add(new BookResponseDto("Test Title3", "Test Content3", "Test Writer3"));
//stub
BDDMockito.given(bookService.getBooks()).willReturn(resultList);
//when
mockMvc.perform(MockMvcRequestBuilders.get("/books")
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk());
//then
BDDMockito.verify(bookService).getBooks();
}
일단은 저같이 static 메소드를 바로 알아보기 힘들어서 헤맬분들을 위하여 static import를 하지 않고 그대로 사용하도록 코드를 작성하였습니다.
다음처럼 요청을 보내었을경우, 반환되는 상태코드와 verify()를 통하여 실행되었는지 확인하는 테스트 코드를 작성하였습니다.
다음처럼 테스트가 정상적으로 동작하는 것 또한 확인할 수 있었습니다.
그 다음으로는 단일 조회 테스트 코드를 작성해보겠습니다.
@Test
@DisplayName("책 단일 조회 테스트")
public void getBookTest() throws Exception {
//given
Long bookId = 1L;
BookResponseDto responseDto =
new BookResponseDto("Test Title1", "Test Content1", "Test Writer1");
//stub
BDDMockito.given(bookService.getBook(bookId)).willReturn(responseDto);
//when
mockMvc.perform(MockMvcRequestBuilders.get("/book/" + bookId)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk());
//then
BDDMockito.verify(bookService).getBook(bookId);
}
다음처럼 특정 Id를 통하여 하나의 책을 조회하는 단일 조회 기능의 테스트코드 또한 작성해보았습니다.
PathVariable이 적용되어 있어서 어떻게 코드를 작성할까 고민하였는데, 일단은 "/book/" + bookId의 꼴로 작성하였습니다.
검색해본 뒤에 수정해볼 생각입니다.
테스트 코드 역시 정상적으로 잘 동작됨을 확인할 수 있었습니다.
@Test
@DisplayName("책 저장 테스트")
public void saveBookTest() throws Exception{
//given
//stub
BDDMockito.given(bookService.saveBook(BDDMockito.any())).willReturn(BDDMockito.anyLong());
//when
mockMvc.perform(MockMvcRequestBuilders.post("/books")
.content(objectMapper.writeValueAsString(saveDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isCreated());
//then
BDDMockito.verify(bookService).saveBook(BDDMockito.any());
}
가장 많은 시간을 잡아먹었던 테스트 코드입니다.
기존의 코드는 이러하였습니다.
@Test
@DisplayName("책 저장 테스트")
public void saveBookTest() throws Exception{
//given
BookSaveDto saveDto = new BookSaveDto("Test Title", "Test Content", "Test Author");
//stub
BDDMockito.given(bookService.saveBook(saveDto)).willReturn(BDDMockito.anyLong());
//when
mockMvc.perform(MockMvcRequestBuilders.post("/books")
.content(objectMapper.writeValueAsString(saveDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isCreated());
//then
BDDMockito.verify(bookService).saveBook(saveDto);
}
다음처럼 DTO 파일을 생성한 뒤, Long형 정수를 반환받는 형식으로 테스트 코드를 작성하였으나,
다음의 오류가 발생하면서 테스트에 실패하였습니다.
로그만으로 보았을 때에는 참조값이 맞지 않아서 이러한 문제가 발생하는 것 같은데, 왜인지는 잘 모르겠습니다.
따라서, 이러한 오류를 없애기 위하여 any() 메소드를 사용하여 테스트 코드를 작성하였습니다.
다음처럼 테스트 코드가 잘 동작하는 것을 확인할 수 있었습니다.
책 수정 테스트 코드를 작성하는 경우에도 다음처럼
오류가 발생하는 것을 확인할 수 있었습니다.
그래서 그냥 다음의 어노테이션을 추가해줌으로써, 객체를 비교할 때 값을 대상을 비교하도록 설정하였습니다.
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@EqualsAndHashCode // 해당 어노테이션 추가
public class BookEditDto {
private String title;
private String content;
}
따라서, 이를 바탕으로 작성한 테스트 코드는 다음과 같습니다.
@Test
@DisplayName("책 수정 테스트")
public void editBookTest() throws Exception{
//given
Long bookId = 1L;
BookEditDto editDto = new BookEditDto("Edit Title", "Edit Content");
//stub
BDDMockito.given(bookService.editBook(editDto, bookId)).willReturn(bookId);
//when
mockMvc.perform(MockMvcRequestBuilders.put("/book/" + bookId)
.content(objectMapper.writeValueAsString(editDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk());
//then
BDDMockito.verify(bookService).editBook(editDto, bookId);
}
테스트 코드가 잘 동작하는 것을 확인할 수 있었습니다.
@Test
@DisplayName("책 삭제 테스트")
public void deleteBookTest() throws Exception {
//given
Long bookId = 1L;
//stub
BDDMockito.willDoNothing().given(bookService).deleteBook(bookId);
//when
mockMvc.perform(MockMvcRequestBuilders.delete("/book/" + bookId))
.andExpect(MockMvcResultMatchers.status().isOk());
//then
BDDMockito.verify(bookService).deleteBook(bookId);
}
책 삭제 테스트의 경우, 다음처럼 사용자가 제공하는 Id값을 통하여 책 삭제 로직이 실행되는지 확인하는 테스트를 수행하도록 코드를 작성하였습니다.
다음처럼 테스트가 잘 동작함을 확인할 수 있었습니다.
다음처럼 전체적으로 테스트를 수행하였을 때, 모든 테스트가 정상적으로 동작함을 확인할 수 있었습니다.
솔직히 테스트 코드를 이론적으로 알고만 있을 때에는 테스트 코드의 필요성이 피부에 와닿지는 않았습니다.
그냥 서버를 켰다, 껐다를 반복하면서 값을 비교하기에는 귀찮기때문에 테스트 코드를 작성함으로써 값의 변화를 관찰하려는 것이 주된 목표라고만 생각하였습니다.
그러나, 간단하게나마 테스트 코드를 작성해보고나니, 코드를 작성하는데에 있어서 어떠한 점이 부족하고, 코드를 어떻게 작성해야하는지 느낌이 오기 시작하였습니다.
왜 코드를 작성할 때 기능을 중점적으로 나누어서 설계를 해야하는지, 왜 Entity에 Entity와 관련된 기능들을 넣어주는지에 대한 의문들이 해소되었으며, 테스트 코드를 작성하면서 코드의 설계 또한 어떻게 신경써주어야하는지에 대해서 알게 되었습니다.
솔직히 테스트 코드 공부하면서 그렇게 큰 기대는 안했었는데, 생각보다 많은 부분들을 배우고 가는 것 같습니다.
앞으로 여러가지 개인 프로젝트들을 진행해가면서 성장했으면 좋겠습니다.
https://okky.kr/articles/1152885
https://junha.tistory.com/30