티스토리 뷰

728x90

테스트코드와 TDD에 대해 책도 보고 구글링도 해봤지만 그래서 난 뭘 해야하는지가 감이 안와서

회사에서 제가 작업했던 API에 대해 테스트코드를 작성해봤습니다.

회사 코드를 공개할 순 없으니, 샘플용으로 간단한 API를 설계해보고 그걸로 같이 테스트 코드를 작성해보고자 합니다.

 

부족한 점이 많습니다. 잘못됐거나 보강 필요한 부분은 댓글 부탁드려요.


빵집의 상품 목록 조회 API

개요

  • EndPoint : /bread/list
  • Method : GET
  • Parameter : breadName (빵 검색어)

 

Response Body

{
  "code": 0,
  "message": "success",
  "response": {
    "statusCode": 200,
    "requestTime": "2022-03-13 16:34:44",
    "data": [
      {
        "breadId": 1,
        "breadName": "크로와상",
        "price": 3500
      },
      {
        "breadId": 2,
        "breadName": "우유식빵",
        "price": 4000
      },
      {
        "breadId": 3,
        "breadName": "밤식빵",
        "price": 4500
      }
    ]
  }
}

 

Controller

@RestController
@RequiredArgsConstructor
public class BreadListController {
    private final BreadListService breadListService;

    @GetMapping("/bread/list")
    public ResponseEntity getBreadList(@RequestParam String breadName){
        List<Bread> breadList = breadListService.getPreadList(breadName);
        return ResponseEntity.ok().body(breadList);
    }

}

컨트롤러 테스트코드 작성하기

1. @WebMvcTest 어노테이션

@WebMvcTest(BreadListController.class)
public class BreadListControllerTest {
  • 여러 스프링 어노테이션 중, Web(SpringMVC)에 집중할 수 있는 어노테이션
  • 선언할 경우 @Controller, @ControllerAdvice 등을 사용할 수 있음
  • 단, @Service, @Component, @Repository 등은 사용 불가
  • 컨트롤러 테스트이므로 선언하였음

2. MockMvc

private MockMvc mvc;
private BreadListService breadListService;
private List<Bread> breadList;
  • 웹 API 테스트 시 사용
  • 스프링 MVC 테스트의 시작점
  • MockMvc를 통해 HTTP GET, POST 등에 대한 API테스트가 가능

3. StaticApplicationContext

private static final StaticApplicationContext applicationContext = new StaticApplicationContext();
private static final WebMvcConfigurationSupport webMvcConfigurationSupport = new WebMvcConfigurationSupport();
  • StaticApplicationContext : Bean 메타정보를 등록
  • WebMvcConfigurationSupport : MVC Java config기능을 제공. 회사에서는 GlobalExceptionHandler를 추가하기 위해 사용하였음. 이 두 코드를 처음엔 뺐는데 그랬더니 에러가 났다.
    구글링 해보니 @EnableMvc어노테이션을 붙이면 WebMvcConfigurationSupport클래스가 자동으로 빈등록이 된다고 한다.

 

4. BeforeEach

테스트 실행 전 셋업

@BeforeEach
void setUp(){
    breadListService = Mockito.mock(BreadListService.class);
    webMvcConfigurationSupport.setApplicationContext(applicationContext);
    mvc = MockMvcBuilders.standaloneSetup(new BreadListController(breadListService))
            .alwaysDo(print())
            .build();

    Bread bread1 = Bread.builder()
            .breadId(1)
            .breadName("크로와상")
            .price(3500)
            .build();
    Bread bread2 = Bread.builder()
            .breadId(2)
            .breadName("우유식빵")
            .price(4000)
            .build();
    Bread bread3 = Bread.builder()
            .breadId(3)
            .breadName("밤식빵")
            .price(4500)
            .build();

    breadList = new ArrayList<>();
    breadList.add(bread1);
    breadList.add(bread2);
    breadList.add(bread3);
}
  • breadListService에 Mock 객체 주입 (Mockito 사용)
  • MockMvcBuilder를 사용하여 BreadListController에 서비스빈을 생성자 주입하면서 MockMvc 인스턴스 생성
  • alwaysDo(print()) 하면 요청에 대한 응답 결과를 콘솔에 찍어줌 (공식문서 참고)
  • 빵목록 데이터 임시 생성
MockMvcBuilder에는 standaloneSetup말고도 webAppContextSetup이 있습니다. 둘의 차이는 아래 글을 참고해주세요.
https://velog.io/@hanblueblue/Spring-mvc-standaloneSetup-vs-webAppContextSetup

 

 

5. 테스트

@Test
@DisplayName("빵 목록 조회 API를 호출한다")
public void 빵_목록_조회_컨트롤러_테스트() throws Exception {
    // given
    given(breadListService.getBreadList(any())).willReturn(breadList);

    // when & then
    mvc.perform(
            get("/bread/list")
    )
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code", is(0)))
            .andExpect(jsonPath("$.message", is("success")))
            .andExpect(jsonPath("$.response.statusCode", is(200)))
            .andExpect(jsonPath("$.response.requestTime", is(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.KOREA).format(LocalDateTime.now()))))
            .andExpect(jsonPath("$.response.data[0].breadId", is(breadList.get(0).getBreadId())))
            .andExpect(jsonPath("$.response.data[0].breadName", is(breadList.get(0).getBreadName())))
            .andExpect(jsonPath("$.response.data[0].price", is(breadList.get(0).getPrice())))
            .andExpect(jsonPath("$.response.data[1].breadId", is(breadList.get(1).getBreadId())))
            .andExpect(jsonPath("$.response.data[1].breadName", is(breadList.get(1).getBreadName())))
            .andExpect(jsonPath("$.response.data[1].price", is(breadList.get(1).getPrice())))
            .andExpect(jsonPath("$.response.data[2].breadId", is(breadList.get(2).getBreadId())))
            .andExpect(jsonPath("$.response.data[2].breadName", is(breadList.get(2).getBreadName())))
            .andExpect(jsonPath("$.response.data[2].price", is(breadList.get(2).getPrice())));

}
  • Given-When-Then 패턴
    • 준비 - 실행 - 검증
    • Given : 테스트를 위한 준비 작업. 여기서는 미리 만들어둔 빵목록을 삽입
    • When : 테스트 하고자 하는 오퍼레이션을 실행. 여기서는 mvc.perform()을 사용하여 /bread/list api를 호출
    • Then : 테스트 결과를 검증
Given-When-Then 패턴에 대해 자세히 알고 싶으시면 아래 글을 참고해주세요.
https://brunch.co.kr/@springboot/292
  • mvc.perform()
    • MockMvc를 통해 HTTP 요청
    • 체이닝이 지원되어 파라미터, reuqestbody 등 추가 가능
// requestParam 추가 예시
mvc.perform(
        get("/bread/list")
                .param("breadName", "크로와상")
)

// post 요청 예시
mvc.perform(
    post("/product/create")
        .content(objectMapper.writeValueAsString(requestDto)) // requestBody
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON)
)
  • andExpected(status().isOk())
    • mvc.perform의 결과를 검증
    • HTTP Header의 Status(ex. 200, 404, 500)를 검증
    • 여기서는 ok, 즉 200인지 아닌지를 검증함
  • jsonPath : json객체를 탐색하기 위한 표준화된 방법. 여기서는 responsebody를 탐색하는 용으로 사용

트러블슈팅

NullPointerException

  • 문제 : Mock빈 주입했음에도 객체가 들어가지 않았음.
  • 원인 : Test어노테이션의 패키지 경로가 잘못되었음.
  • 해결
// as is
import org.junit.Test;

// to be
import org.junit.jupiter.api.Test;

 

전체 코드

import 참고 용으로 같이 공유합니다

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.hamcrest.CoreMatchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(BreadListController.class)
public class BreadListControllerTest {
    private MockMvc mvc;
    private BreadListService breadListService;
    private List<Bread> breadList;
    
    private static final StaticApplicationContext applicationContext = new StaticApplicationContext();
    private static final WebMvcConfigurationSupport webMvcConfigurationSupport = new WebMvcConfigurationSupport();

    @BeforeEach
    void setUp(){
        breadListService = Mockito.mock(BreadListService.class);
        // Mock Bean 생성
        
		webMvcConfigurationSupport.setApplicationContext(applicationContext);
        mvc = MockMvcBuilders.standaloneSetup(new BreadListController(breadListService))
                .alwaysDo(print())
                .build();

        Bread bread1 = Bread.builder()
                .breadId(1)
                .breadName("크로와상")
                .price(3500)
                .build();
        Bread bread2 = Bread.builder()
                .breadId(2)
                .breadName("우유식빵")
                .price(4000)
                .build();
        Bread bread3 = Bread.builder()
                .breadId(3)
                .breadName("밤식빵")
                .price(4500)
                .build();

        breadList = new ArrayList<>();
        breadList.add(bread1);
        breadList.add(bread2);
        breadList.add(bread3);
    }

    @Test
    @DisplayName("빵 목록 조회 API를 호출한다")
    public void 빵_목록_조회_컨트롤러_테스트() throws Exception {
        // given
        given(breadListService.getBreadList(any())).willReturn(breadList);

        // when & then
        mvc.perform(
                get("/bread/list")
        )
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code", is(0)))
                .andExpect(jsonPath("$.message", is("success")))
                .andExpect(jsonPath("$.response.statusCode", is(200)))
                .andExpect(jsonPath("$.response.requestTime", is(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.KOREA).format(LocalDateTime.now()))))
                .andExpect(jsonPath("$.response.data[0].breadId", is(breadList.get(0).getBreadId())))
                .andExpect(jsonPath("$.response.data[0].breadName", is(breadList.get(0).getBreadName())))
                .andExpect(jsonPath("$.response.data[0].price", is(breadList.get(0).getPrice())))
                .andExpect(jsonPath("$.response.data[1].breadId", is(breadList.get(1).getBreadId())))
                .andExpect(jsonPath("$.response.data[1].breadName", is(breadList.get(1).getBreadName())))
                .andExpect(jsonPath("$.response.data[1].price", is(breadList.get(1).getPrice())))
                .andExpect(jsonPath("$.response.data[2].breadId", is(breadList.get(2).getBreadId())))
                .andExpect(jsonPath("$.response.data[2].breadName", is(breadList.get(2).getBreadName())))
                .andExpect(jsonPath("$.response.data[2].price", is(breadList.get(2).getPrice())));

    }
}

 

 

참고자료

http://www.yes24.com/Product/Goods/83849117

https://velog.io/@dsunni/Spring-Boot-02-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C

https://ykh6242.tistory.com/100

 

 

728x90
댓글
300x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함