티스토리 뷰
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
'Java, Spring' 카테고리의 다른 글
스프링부트의 다중요청 처리 - Tomcat9 Thread Pool & JAVA NIO (0) | 2022.05.01 |
---|---|
[Apache POI] SXSSF의 메모리 관리법 : 슬라이딩 윈도우(Sliding Window) (0) | 2022.04.28 |
Java Bean과 Spring Bean (0) | 2022.04.16 |
테스트코드 관련 개념 정리 - xUnit, JUnit, Mockito, AssertJ, TDD 등 (1) | 2022.03.26 |
[Apache POI] 다량의 데이터 엑셀 다운로드 처리로 인한 서버 장애 대응 후기 (0) | 2022.02.26 |
댓글
300x250
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- laravel 테스트코드
- AOP
- laravel
- MySQL
- kafka
- k8s
- php
- phpUnit
- Container
- Infra
- 대규모 데이터 처리
- 샤딩
- devops
- 라라벨
- springboot
- 스프링
- 카프카
- JUnit
- Spring
- database
- docker
- java
- index
- kubernetes
- 쿠버네티스
- 몽고디비
- 분산처리
- NoSQL
- mockery
- mongoDB
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함