프로젝트나 사람들과 협업을 하게되면 테스트 코드는 필수적이다.
테스트코드를 작성하는 이유는 다음과 같다.
- 개발단계 초기에 문제를 발견하게 도와준다.
- 개발자가 나중에 코드를 리팩토링 할 때 기존 기능의 올바르게 수행되는지 확인할 수 있다.
- 기능에 대한 불확실성을 감소시킨다.
- 시스템에 대한 실제 문서를 제공해준다.
@SpringBootApplication
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}
스프링부트 웹을 실행할 수 있는 이유
@SpringBootApplicaiton은 스프링부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정해준다. 이로 인해 내장 WAS(Web Application Server,웹 애플리케이션 서버)를 실행할 수 있다. 내장 WAS란 외부에 WAS를 두지 않고 애플리케이션을 실행시킬때, 내부에서 WAS를 실행하는 것을 의미한다.
Junit 테스트 코드 작성
build.gradle에 의존성 추가
테스트 코드(Controller & Service)
스프링 처음 공부할 때 만든 CRUD기능을 이용한 게시판 기능들에 대한 테스트 코드를 작성하였다.
BoardControllerTest
package com.example.board;
import com.example.board.controller.BoardController;
import com.example.board.responsedto.BoardEditRequestDto;
import com.example.board.responsedto.BoardRequestDto;
import com.example.board.service.BoardService;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ExtendWith(MockitoExtension.class)
public class BoardControllerTest {
@InjectMocks
BoardController boardController;
@Mock
BoardService boardService;
MockMvc mockMvc;
ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
public void beforeEach()
{
mockMvc = MockMvcBuilders.standaloneSetup(boardController).build();
}
@Test
@DisplayName("writeTest")
public void saveBoardTest() throws Exception
{
// given
BoardRequestDto boardReq = new BoardRequestDto("제목","내용","작성자");
// when, then
mockMvc.perform(
post("/board")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(boardReq)))//object->String type으로 변환
.andExpect(status().isOk());
verify(boardService).save(boardReq);
}
@Test
@DisplayName("getAllTests")
public void findBoardsTest() throws Exception{
mockMvc.perform(get("/board"))
.andExpect(status().isOk());
verify(boardService).getBoards();
}
@Test
@DisplayName("getTest")
public void findBoardTest() throws Exception{
Long id = 1L;
mockMvc.perform(get("/board/{id}",id))
.andExpect(status().isOk());
verify(boardService).getBoard(id);
}
@Test
@DisplayName("editTest")
public void editBoardTest() throws Exception{
Long id = 1L;
BoardEditRequestDto boardEditReq = new BoardEditRequestDto("제목","내용","홍길동");
mockMvc.perform(put("/board/{id}",id)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(boardEditReq)))
.andExpect(status().isOk());
verify(boardService).editBoard(id, boardEditReq);
assertThat(boardEditReq.getTitle()).isEqualTo("제목");
}
@Test
@DisplayName("deleteTest")
public void deleteBoardTest() throws Exception{
Long id = 1L;
mockMvc.perform(
delete("/board/{id}",id))
.andExpect(status().isOk());
verify(boardService).deleteBoard(id);
}
}
Mock 객체란 ?
- 실제 객체를 다양한 조건으로 인해 제대로 구현하기 어려운 경우 임의의 객체를 생성하는 것인데, 이를 Mock 객체라고 한다.
Mock 객체는 언제 사용할까?
- 테스트 작성을 위한 환경을 만들기 어려울 때
- 테스트가 특정 경우나 순간에 의존적인 경우
MockMvc mockmvc
- 웹 API를 테스트 할때 사용한다.
- 스프링 MVC 테스트의 시작점이다.
- 이 클래스를 통해 HTTP, GET, POST 등에 대한 API 테스트를 할 수 있다.
@ExtendWith : Junit5의 확장 기능 사용(스프링 연동 테스트를 가능하게 한다)
@Test : 독립적인 단위 테스트를 위해 작성
@displayName : 테스트 실행시 표기되는 테스트의 이름
@BeforeEach : 테스트 메소드가 실행되기 이전에 딱 한번 수행한다.
mockMvc = MockMvcBuilders.standaloneSetup(boardController).build()=> 테스트를 하기 위한 mockMvc 객체
ObjectMapper : JSON, Java간의 직,병렬화를 위해 사용
JSON : "키 : 값" 으로 이루어진 데이터 객체를 전달하기 위해 사람이 읽을 수 있는 텍스트를 사용하는 포맷이다.
직렬화(serialize) : 데이터 전송 및 저장을 위해 객체를 문자열로 변환하는 과정(Object->String)
병렬화(deserialize) : 데이터가 전송된 이후 전달된 데이터를 다시 객체로 되돌려 주는 과정(String ->Object)
mockMvc.perfotm(get("/board")
- mockMvc 객체를 통해 board주소로 HTTP get요청을 한다.
- 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언할 수 있다.
andExpect(status().isOk())
- mvc.perform의 결과를 검증한다. HTTP Header의 Status검증 (200, 404, 500 등 점검)
writeValueAsString(value) => Object -> String type으로 바꾸기 위함
verify(T mock).method(); => mock 객체의 원하는 메소드가 성공적으로 실행되었는지 검증하는 단계이다.
verify(boardService).save(boardReq); => 이 코드에서는 boardService mock 객체가 boardReq 객체 에 담겨있는 값들이 잘 저장되었는지 검증하는 것이다.
@WebMvcTest :
- 스프링 어노테이션 중, Web에만 집중할 수 있게 한다.
- @Controller, @ControllerAdvice만 사용 가능하다.
- @Service, @Component, @Repository는 사용 불가능하다.
@Extendwith(MockitoExtension.clasS)=>Mockito에서 제공하는 Mock 객체를 사용하기 위해 사용하였다.
@Mock @InjectMocks : @InjectMocks가 붙은 객체에 @Mock를 주입시킬 수 있다.
테스트 코드의 작성 요령
given -> when -> then 순서이다.
준비 -> 실행 -> 검증 순서이다.
Given(준비)
테스트에 사용되는 변수, 입력값 및 Mock 객체를 선언하는 부분은 준비단계 이다.
when(실행)
테스트를 실행하는 부분이다. (테스트코드의 주요 기능 메서드를 실행)
Then(검증)
예상한 대로 값이 나왔는지 확인시켜주는 단계이다.
BoardServiceTest
package com.example.board;
import com.example.board.entity.Board;
import com.example.board.repository.BoardRepository;
import com.example.board.responsedto.BoardEditRequestDto;
import com.example.board.responsedto.BoardRequestDto;
import com.example.board.responsedto.BoardResponseDto;
import com.example.board.service.BoardService;
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 java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
public class BoardServiceTest {
@InjectMocks
BoardService boardService;
@Mock
BoardRepository boardRepository;
@Test
@DisplayName("getAllTests")
void findBoardsServiceTest()
{
List<Board> boards = new ArrayList<>();
Board board = new Board("제목","내용","작성자");
Board board2 = new Board("제목","내용","작성자");
boards.add(board);
boards.add(board2);
given(boardRepository.findAll()).willReturn(boards);
List<BoardResponseDto> result = boardService.getBoards();
assertThat(result.size()).isEqualTo(2);
}
@Test
@DisplayName("getTest")
void getBoardServiceTest()
{
//given
Board board = new Board("제목", "내용","작성자");
given(boardRepository.findById(anyLong())).willReturn(Optional.of(board));
//when
BoardResponseDto result = boardService.getBoard(1L);
//then
assertThat(result.getTitleDto()).isEqualTo(board.getTitle());
}
@Test
@DisplayName("writeTest")
void saveBoardServiceTest(){
//given
Board board = new Board("제목","내용","작성자");
BoardRequestDto boardRequestDto = new BoardRequestDto("제목","내용","작성자");
given(boardRepository.save(board)).willReturn(board);
//when
boardService.save(boardRequestDto);
//then
verify(boardRepository).save(any());
}
@Test
@DisplayName("editTest")
void editBoardServiceTest()
{
Board board = new Board(1L,"제목","내용","작성자");
BoardEditRequestDto req = new BoardEditRequestDto("제목","내용2","작성자");
given(boardRepository.findById(anyLong())).willReturn(Optional.of(board));
boardService.editBoard(1L,req);
assertThat(board.getContent()).isEqualTo("내용2");
}
@Test
@DisplayName("deleteTesting")
void deleteBoardServiceTest()
{
Board board = new Board("제목","내용","작성자");
given(boardRepository.findById(anyLong())).willReturn(Optional.of(board));
boardService.deleteBoard(1L);
verify(boardRepository).deleteById(anyLong());
}
}
any(), anyLong()
- Service를 구현한 클래스를 테스트하기 위한 인자값을 넣기 위해 any(), anyLong()을 사용한다.
- 특정 인자 값을 사용하는 경우도 존재하다.
테스트 코드 결과물
각 서비스 및 컨트롤러의 기능들마다 테스트를 수행하는데 걸린 시간과 체크표시가 나오게 된다.
프로젝트를 만들기 위해서는 테스트코드를 통해 자신이 만든 프로젝트가 올바른 기능을 수행하고 있는지 생각해 볼 필요가 있을 것 같다.
<참고 자료>
https://escapefromcoding.tistory.com/341
https://brunch.co.kr/@springboot/292
https://cornswrold.tistory.com/369
https://blog.naver.com/sosow0212/222838356590
'Java > Spring' 카테고리의 다른 글
스프링부트 프로젝트 최종 결과물 (0) | 2022.08.22 |
---|---|
스프링 프로젝트 리팩토링 전 코드(review) (0) | 2022.08.22 |
SpringBoot JPA(Java Persistence API) 사용 목적 (0) | 2022.08.05 |
DTO, DAO, Repository, Entity 개념 (0) | 2022.08.04 |
예외처리(Exception)기능 + Response 기능(데이터 반환) 을 추가한 게시판 만들기 코드 (2) | 2022.07.27 |