Java/Spring

Junit5을 이용한 테스트코드 작성(단위 테스트 코드)

SeungbeomKim 2022. 8. 15. 22:15

프로젝트나 사람들과 협업을 하게되면 테스트 코드는 필수적이다.

테스트코드를 작성하는 이유는 다음과 같다.

  • 개발단계 초기에 문제를 발견하게 도와준다.
  • 개발자가 나중에 코드를 리팩토링 할 때 기존 기능의 올바르게 수행되는지 확인할 수 있다.
  • 기능에 대한 불확실성을 감소시킨다.
  • 시스템에 대한 실제 문서를 제공해준다. 
@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 

 

ObjectMapper

ObjectMapper란? JSON 형식을 사용할 때, 응답들을 직렬화하고 요청들을 역직렬화 할 때 사용하는 기술이다. (*여기서 다소 생소한 JSON 형식, 직렬화, 역직렬화를 잠깐 살펴본다.) JSON(Javascript Object Notat

escapefromcoding.tistory.com

https://brunch.co.kr/@springboot/292

 

Given-When-Then Pattern

테스트 코드 작성 표현 방법 (스프링 부트 환경에서) | 이번 글에서는, 테스트 코드 작성 시 자주 사용하는 Given-When-Then Pattern에 대해서 간략하게 소개하겠다. 별 내용 없는 글이므로, 아주 편한

brunch.co.kr

https://cornswrold.tistory.com/369

 

Mockito 어노테이션(@Mock, @InjectMocks)

Mockito 관련 어노테이션 @RunWith(MockitoJunitRunner.class) Mockito에서 제공하는 목객체를 사용하기 하기위해 위와같은 어노테이션을 테스트클래스에 달아준다. @RunWith(MockitoJunitRunner.class) public cl..

cornswrold.tistory.com

https://heegs.tistory.com/16

 

[Mockito] Mock 객체 란?

Mock 이란? 실제 객체를 다양한 조건으로 인해 제대로 구현하기 어려울 경우 가짜 객체를 만들어 사용하는데, 이를 Mock 객체라 한다. Mock 객체가 필요한 경우. 테스트 작성을 위한 환경 구축이 어

heegs.tistory.com

https://kogle.tistory.com/264

 

Spring Test : 테스트 시 인자로 any or 특정 값?

1. 테스트를 하다 보면 언제 any, anyLong 같은 값을 사용할지 아니면 진짜 값을 넣어주어야 할지 혼란스럽다. 2. 아래 같은 유닛테스트의 경우는 모두 any, anyLong을 사용하고 있다. 2-1 예제는 Service 구

kogle.tistory.com

https://blog.naver.com/sosow0212/222838356590

 

[JUnit5] 스프링부트 단위 테스트 작성 예시(Controller & Service)

[JUnit5] 스프링부트 단위 테스트를 작성 예시 (Controller & Service) https://github.com/sosow0...

blog.naver.com