
이전 포스트에서는 Test Code를 작성 이유와 어떻게 작성하는지 포스트했다면 이번 글에서 부터는 실직적으로 Test Code 작성하는 방법에 대해서 포스트 하겠다.
package com.springboot.test;
import org.junit.jupiter.api.*;
public class TestLifeCycle {
@BeforeAll //
static void beforeAll() {
System.out.println("## Before All Annotation 호출 ##");
System.out.println();
}
@AfterAll
static void afterAll() {
System.out.println("## After All Annotation ##");
System.out.println();
}
@BeforeEach
void beforeEach() {
System.out.println("## Before Each Annotation ##");
System.out.println();
}
@AfterEach
void afterEach() {
System.out.println("## After Each Annotation ##");
System.out.println();
}
@Test
void test1() {
System.out.println("## test1 시작 ##");
System.out.println();
}
@Test
void test2() {
System.out.println("## test2 시작 ##");
System.out.println();
}
@Test
@Disabled
void test3() {
System.out.println("## test3 시작 ##");
System.out.println();
}
}
## Before All Annotation 호출 ##
## Before Each Annotation ##
## test1 시작 ##
## After Each Annotation ##
## Before Each Annotation ##
## test2 시작 ##
## After Each Annotation ##
## After All Annotation ##
위의 어노테이션에 따른 TestCode 실행되는 모습으로 더 알기 쉽게 알수 가 있습니다.
컨트롤러는 클라이언트로부터 요청을 받아 요청에 걸맞은 서비스 컴포넌트로 요청을 전달하고 그 결괏값을 가공해서 클라이언트에게 응답하는 역할을 수행
주의 사항
package com.springboot.test.controller;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.service.ProductService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
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(ProductController.class)
public class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
ProductService productService;
@Test
@DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
void getProduct() throws Exception {
given(productService.getProduct(123L)).willReturn(
new ProductResponseDto(123L, "pen", 5000, 2000)
);
String productId = "123";
mockMvc.perform(
get("/product?number=" + productId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.number").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andDo(print());
verify(productService).getProduct(123L);
}
}
@SpringBootTest
실제 애플리케이션을 자신의 로컬 위에 올려서 포트 주소나 Listening 되어지고, 실제 Database와 커넥션이 붙어지는 상태에서 진행되는 Live 테스트 방법@WebMvcTest
Controller(API) Layer만을 테스트하기 적합한 테스트 어노테이션으로 전체 애플리케이션을 실행하는 것이 아닌 Controller만을 로드하여 테스트를 진행할 수 있다.차이점 :
- @SpringBootTestsms 전체 애플리케이션을 띄워서 실행
- @WebMvcTest Controller만 띄워서 실행
애플리케이션의 규모가 커지게 되는 경우 테스트 시간이 그만큼 길어지기 때문에 신규 기능이나 버그 패치 등 일부 기능만 테스트하고자 할 때는
WebMvcTest가 적당합니다.
일반적으로 @WebMvcTest 어노테이션을 사용한 테스트는 슬라이스(Slice) 테스트라고 부릅니다.
슬라이스 테스트는 단위 테스트와 통합 테스트의 중간 개념으로 이해하면 되는데, 레이어드 아키텍처를 기준으로 각 레이어별로 나누어 테스트를 진행한다는 의미입니다.
컨트롤러는 개념상 웹과 맞닿은 레이어로서 외부 요인을 차단하고 테스트하면 의미가 없기 때문에 슬라이스 테스트를 진행하는 경우가 많다.
예제 코드 25~26번 줄에서 @MockBean 어노테이션을 통해 ProductController가 의존성을 가지고 있던 ProductService 객체에 Mock 객체를 주입했습니다. Mock 객체에는 테스트 과정에서 맡을 역할을 32~33번 줄과 같이 Mockito에서 제공하는 given() 메서드를 통해 이 객체에서 어떤 메서드가 호출되고 어떤 파라미터를 주입받는지 가정한 후 willReturn() 메서드를 통해 어떤 결과를 리턴할 것인지 정의하는 구조로 코드를 작성
⇒ Given에 해당
23번 줄에서 사용된 MockMvc는 컨트롤러의 API를 테스트하기 위해 사용된 객체입니다. 정확하게는 서브릿 컨테이너의 구동 없이 가상의 MVC 환경에서 모의 HTTP 서블릿을 요청하는 유틸리티 클래스입니다.
perform() 메서드를 이용하면 서버로 URL 요청을 보내는 것처럼 통신 테스트 코드를 작성해서 컨트롤러를 테스트할 수 있습니다.
perform() 메서드는 MockMvcRequestBuilders에서 제공하는 HTTP 메서드로 URL을 정의해서 사용합니다.
MockMvcRequestBuilders는 GET, POST, PUT, DELETE에 매핑되는 메서드를 제공합니다.
이 메서드는 MockHttpServletRequestBuilder 객체를 리턴하며, HTTP 요청 정보를 설정할 수 있게됩니다.
perform() 메서드의 결괏값으로 ResultActions 객체가 리턴되는데, 39~44번 줄과 같이 andExpect() 메서드를 사용해 결괏값 검증을 수행할 수 있습니다.
andExpect() 메서드에서는 ResultMatcher를 활용하는데, 이를 위해 MockMvcResultMatchers 클래스에 정의대 있는 메서드를 활용해 생성할 수 있습니다.
요청과 응답의 전체 내용을 확인하려면 45번 줄과 같이 andDo() 메서드를 사용합니다. MockMvc의 코드는 모두 합쳐져 있어 구분하기 애매하지만 전체적인 ‘When-Then’의 구조를 갖추고 있음을 확인할 수 있습니다.
마지막으로 verify() 메서드는 지정된 메서드가 실행됬는지 검증하는 역할입니다. 일반적으로 given()에 정의된 동작과 대응합니다.
test/com.springboot.test.controller/ProductControllerTest.java
package com.springboot.test.controller;
import com.springboot.test.data.dto.ProductDto;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.service.ProductService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
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(ProductController.class)
public class ProductControllerTest {
/**
* MockMvc 가짜 Service
*/
@Autowired
private MockMvc mockMvc;
@MockBean
ProductService productService;
@Test
@DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
void getProduct() throws Exception {
//Mock 객체에서 특정 메서드가 실행되는 경우 실제 Return을 줄 수 없기 때문에 아래와 같이 가정 사항을 만들어줌
given(productService.saveProduct(new ProductDto("pen", 5000, 2000)))
.willReturn(new ProductResponseDto(12315L, "pen", 5000, 2000));
ProductDto productDto = ProductDto.builder()
.name("pen")
.price(5000)
.stock(2000)
.build();
Gson gson = new Gson();
String content = gson.toJson(productDto);
mockMvc.perform(
post("/product/post").content(content).contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.number").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
// verity() 실행됐는지 검증하는 역할
verify(productService).saveProduct(new ProductDto("pen", 5000, 2000));
}
}
data/dto/ProductDto.java
package com.springboot.test.data.dto;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ProductDto {
private String name;
private int price;
private int stock;
}
//Gson 라이브러리
implementation 'com.google.code.gson:gson:2.9.0'
getProduct()를 테스트하는 코드와 거의 비슷하게 구성돼 있습니다.
MockHttpServletRequest:
HTTP Method = POST
Request URI = /product/post
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"40"]
Body = {"name":"pen","price":5000,"stock":2000}
Session Attrs = {}
Handler:
Type = com.springboot.test.controller.ProductController
Method = com.springboot.test.controller.ProductController#createProduct(ProductDto)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"number":12315,"name":"pen","price":5000,"stock":2000}
Forwarded URL = null
Redirected URL = null
Cookies = []
> Task :test
BUILD SUCCESSFUL in 1s
4 actionable tasks: 1 executed, 3 up-to-date
1:37:08 PM: Execution finished ':test --tests "com.springboot.test.controller.ProductControllerTest"'.
성공적으로 테스트가 실행된 것을 확인할 수 있다.
HTTP Method : 요청이 올바른 HTTP 메소드(POST)를 사용하고 있는지 확인합니다.Request URI : 요청된 URI가 올바른지 확인합니다.Headers : 필요한 헤더가 모두 포함되어 있는지 확인합니다.Body : 요청 본문이 예상한 형식과 데이터를 포함하고 있는지 확인합니다.Type : 요청을 처리하는 컨트롤러가 올바른지 확인합니다.Method : 호출된 메서드가 올바른지 확인합니다. Type : 예외가 발생하지 않았는지 또는 발생한 예외가 예상된 것인지 확인합니다.Status : 응답 상태 코드가 200(성공)인지 확인합니다.Headers : 응답 헤더가 올바른지 확인합니다.Content type : 응답의 콘텐츠 타입이 예상한 타입인지 확인합니다.Body : 응답 본문이 예상한 데이터를 포함하고 있는지 확인합니다. BUILD SUCCESSFUL : 빌드가 성공적으로 완료되었는지 확인합니다. [참고]