API

#2 TEST CODE 작성 : Todo API 단위테스트, 통합테스트

letsDoDev 2025. 1. 2. 00:35

https://letsdodev.tistory.com/211
에서 작업했던 Todo API 에서 단위테스트, 통합테스트를 진행한다.

 

#1 TEST CODE 작성 : Todo API 만들기

※ 테스트 코드 작성 이전, 테스트 코드 작성에 필요한 Todo API 작성해서 기록한 게시물※  테스트 코드는 다음 게시물에 작성 예정[프로젝트 생성]- 스프링 이니셜라이저 사용spring-boot : 3.4.0 verja

letsdodev.tistory.com

 
 

@WebMvcTest vs @SpringBootTest

  • @WebMvcTest:
    • 컨트롤러만 로드하고, 그 외의 빈(서비스, 리포지토리 등)은 로드하지 않음
    • 빈 로딩 범위: 컨트롤러만 로드, 서비스나 리포지토리와 같은 다른 빈은 자동 로드되지 않음
    • 주로 웹 계층 테스트에 사용
    • 서비스나 리포지토리 등을 Mock 객체로 주입
    • 예시: @WebMvcTest(TodoController.class) (컨트롤러만 테스트)
  • @SpringBootTest:
    • 전체 애플리케이션 컨텍스트를 로드하므로, 실제 서비스와 리포지토리 빈까지 포함
    • 빈 로딩 범위: 전체 애플리케이션 컨텍스트를 로드 -> 서비스, 리포지토리, 컨트롤러 등이 모두 실제로 로드
    • 실제 클래스를 사용하여 테스트할 수 있음
    • 전체 애플리케이션을 테스트할 때 사용
    • 예시: @SpringBootTest (애플리케이션 전체 테스트)

 

TestRestTemplate VS RestTemplte

목적 실제 HTTP 요청을 보낼 때 사용 테스트 환경에서 HTTP 요청을 시뮬레이션
사용 용도 실제 애플리케이션에서 외부 API 호출 Spring Boot 테스트에서 REST API를 테스트
기능 HTTP 요청과 응답을 처리, 다양한 설정 지원 테스트 환경에서 요청을 보내고 응답을 검증
설정 RestTemplate을 직접 설정해야 함 @SpringBootTest 등에서 자동 설정
디버깅 및 편의성 일반적인 HTTP 클라이언트 기능 제공 테스트 중 디버깅과 설정이 더 간편함


[테스트 전 세팅]
1. 임의 값 INSERT

 
2. DB 체크

 
 
[단위테스트 VS 통합테스트]
단위 테스트 : 가짜 서비스 객체 빈 사용 -> 즉, 컨트롤러 계층까지만 테스트 가능하다는 얘기
통합 테스트 : 실제 서비스 객체 빈 사용 -> 즉, 전체 로직 테스트 가능
 


[#1 단위테스트 :  TodoControllerMockTest.java  + 결과]
- TodoControllerMockTest.java

package com.testcode.jpah2;

import com.testcode.jpah2.todo.controller.TodoController;
import com.testcode.jpah2.todo.dto.ResponseTodoDto;
import com.testcode.jpah2.todo.service.TodoService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

// static import
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.junit.jupiter.api.Assertions.*;

// 단위 테스트 : 가짜 서비스 객체 빈 사용 -> 즉, 컨트롤러 계층까지만 테스트 가능하다는 얘기
@ExtendWith(SpringExtension.class) // junit 5 일 경우 vs junut 4 일 경우 @RunWith(SpringRunner.class)
@WebMvcTest(value = TodoController.class)
public class TodoControllerMockTest {
    // log
    private static final Logger log = LoggerFactory.getLogger(TodoControllerMockTest.class); // 테스트 유형 : 단위 테스트

    @Autowired
    private MockMvc mvc;

    @MockBean
    private TodoService todoService;

    @Test
    public void findAllTodoTest() throws Exception {

        // GIVEN
        List<ResponseTodoDto> mockResponse = new ArrayList<ResponseTodoDto>();

        // CASE 1
        // 예상 결과 (JSON 형식의 문자열 대비 객체)
        String mockDateStr = "2025-01-01 22:46:40.138806";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS");
        ResponseTodoDto mockResponseTodo = new ResponseTodoDto(1L,
                                                            "첫 번째 todo 입력",
                                                            LocalDateTime.parse(mockDateStr, formatter),
                                                     false);
        mockResponse.add(mockResponseTodo);

        // CASE 2
        // String mockResponseTodo = "첫 번째 todo 입력"; // 예상 결과 (이미 DB 에 저장된 값)
        // mockResponse.add(mockResponseTodo);


        String expected = "[{" +
                "\"id\": 1," +
                "\"title\": \"첫 번째 todo 입력\"," +
                "\"date\": \"2025-01-01T22:46:40.138806\"," +
                "\"isFinish\": false" +
                "}]"; // 예상 결과 (JSON 형식의 문자열)

        // WHEN
        // 가짜 빈 todoService 정의
        // test 클래스에서 controller mapping 되는 api 요청을 보낼 때 해당 controller 메소드에서
        // 사용되는 service 의 메소드를 여기서 정의하고, 정의한 서비스 로직이 수행되게 한다.
        // 쉽게 예시 들자면 service 메소드 인터셉트 해서 재정의 후 재정의한 메소드 실행시키는 것.
        /*
        // 타겟 서비스 메소드
         @GetMapping("/findAll")
         public List<ResponseTodoDto> findAllTodo() {
            return todoService.findAllTodo();
         }
        */
        when(todoService.findAllTodo()).thenReturn(mockResponse); // thenReturn 안에 데이터 타입은 실제 TodoService.findAllTodo() 의 반환 데이터 타입과 동일 해야함

        // THEN
        MvcResult result = mvc
                .perform(MockMvcRequestBuilders.request(HttpMethod.GET, "/api/todo/findAll")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andReturn();

        // log.info(expected);
        // log.info(result.getResponse().getContentAsString());

        // CASE 1
        JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false);
        // CASE 2
        // assertEquals(expected, result.getResponse().getContentAsString());
    }

    // 조희 - 실패할
    @Test
    public void findAllTodoTestFailed() throws Exception {

        // GIVEN
        List<ResponseTodoDto> mockResponse = new ArrayList<ResponseTodoDto>();

        // CASE 1
        // 예상 결과 (JSON 형식의 문자열 대비 객체)
        String mockDateStr = "2025-01-01 22:46:40.138806";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS");
        ResponseTodoDto mockResponseTodo = new ResponseTodoDto(1L,
                "첫 번째 todo 입력",
                LocalDateTime.parse(mockDateStr, formatter),
                false);
        mockResponse.add(mockResponseTodo);

        // CASE 2
        // String mockResponseTodo = "첫 번째 todo 입력"; // 예상 결과 (이미 DB 에 저장된 값)
        // mockResponse.add(mockResponseTodo);


        String expected = "[{" +
                "\"id\": 1," +
                "\"title\": \"존재하지 않은 실패할 todo\"," +
                "\"date\": \"2025-01-01T22:46:40.138806\"," +
                "\"isFinish\": false" +
                "}]"; // 예상 결과 (JSON 형식의 문자열)

        // WHEN
        // 가짜 빈 todoService 정의
        // test 클래스에서 controller mapping 되는 api 요청을 보낼 때 해당 controller 메소드에서
        // 사용되는 service 의 메소드를 여기서 정의하고, 정의한 서비스 로직이 수행되게 한다.
        // 쉽게 예시 들자면 service 메소드 인터셉트 해서 재정의 후 재정의한 메소드 실행시키는 것.
        /*
        // 타겟 서비스 메소드
         @GetMapping("/findAll")
         public List<ResponseTodoDto> findAllTodo() {
            return todoService.findAllTodo();
         }
        */
        when(todoService.findAllTodo()).thenReturn(mockResponse); // thenReturn 안에 데이터 타입은 실제 TodoService.findAllTodo() 의 반환 데이터 타입과 동일 해야함

        // THEN
        MvcResult result = mvc
                .perform(MockMvcRequestBuilders.request(HttpMethod.GET, "/api/todo/findAll")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andReturn();

        // log.info(expected);
        // log.info(result.getResponse().getContentAsString());

        // CASE 1
        JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false);
        // CASE 2
        // assertEquals(expected, result.getResponse().getContentAsString());
    }

}

 
- 결과

 
 
[#2 통합테스트 :  TodoControllerSpringBootTest.java  + 결과]
- TodoControllerSpringBootTest.java

package com.testcode.jpah2;

//import com.testcode.jpah2.todo.repository.TodoRepository;
//import com.testcode.jpah2.todo.service.TodoService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;

// static import
import static org.assertj.core.api.Assertions.*;

// 통합 테스트 : 실제 서비스 객체 빈 사용 -> 즉, 전체 로직 테스트 가능
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Jpah2Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerSpringBootTest {

    @Autowired
    private TestRestTemplate testRestTemplate;

    // @SpringBootTest 어노테이션으로 controller, service, repository 모두 빈으로 등록된 상태
    // 현재 테스트에서는 service, repository 를 대상으로 하는 부분 테스트가 아니라
    // api 요청 프로세스 전체 테스트이기 때문에 아래 빈 주입을 하지 않고
    // **restTemplate 으로 api 요청하여 테스트 하였다.
    // 만약, service 나 repository 에 대한 부분 테스트를 진행하려면
    // 아래 주석 처럼 빈 주입을 해주어야 객체의 메소드를 사용할 수 있다.
  
    // @Autowired
    // private TodoService todoService;

    // @Autowired
    // private TodoRepository todoRepository;

    // 조희 - 성공할
    @Test
    public void findAllTodoTest() throws Exception {
        // GIVEN
        /*
        // CASE 1
        String expected = "[{" +
            "\"id\": 1," +
            "\"title\": \"첫 번째 todo 입력"," +
//            "\"date\": \"2025-01-01 22:46:40.138806\"," +
            "\"isFinish\": false" +
            "}]"; // 예상 결과 (JSON 형식의 문자열)
         */

        // CASE 2
        String expected = "첫 번째 todo 입력"; // 예상 결과 (이미 DB 에 저장된 값)

        // WHEN 은 따로 없음 : service 클래스를 MockBean 이 아닌 실제 클래스로 사용할 것이기 떄문
        // THEN
        ResponseEntity<String> response = testRestTemplate.getForEntity("/api/todo/findAll", String.class);

        // CASE 1
        // JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false);
        // CASE 2
        assertThat(response.getBody()).contains(expected);
    }

    // 조희 - 실패할
    @Test
    public void findAllTodoTestFailed() throws Exception {
        // GIVEN
        String expected = "존재하지 않은 실패할 todo"; // 예상 결과 (이미 DB 에 저장된 값)

        // WHEN 은 따로 없음 : service 클래스를 MockBean 이 아닌 실제 클래스로 사용할 것이기 떄문
        // THEN
        ResponseEntity<String> response = testRestTemplate.getForEntity("/api/todo/findAll", String.class);

        // CASE 1
        // JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false);
        // CASE 2
        assertThat(response.getBody()).contains(expected);
    }
}

 
- 결과

 


해당 게시물에서 HttpMethod 타입 중 GET 방식만 다뤘다.
POST, PUT, PATCH 타입도 추후 블로그 포스팅 하겠다.