일반적으로 작은 범위의 단위 테스트만으로는 서비스 전체의 신뢰성, 무결성을 보장할 수 없다. 기능 전체의 정상 작동을 보장하기위해 여러 모듈이 협력하여 제공하는 기능을 통합적으로 테스트해보자.
7.1. E2E 테스트의 범위
End to End 테스트는 서비스의 흐름을 처음부터 끝까지 테스트하는 것을 의미한다. 사용자의 요청에서부터 데이터베이스까지 (혹은 (가능하다면) 외부 연계시스템까지) 모든 구성 요소를 포함하여 테스트를 진행한다. 테스트의 비용이 상대적으로 크더라도 애플리케이션의 무결성을 보장하기 위해 E2E 테스트를 수행하자.
(시작점을 브라우저로 둘 수도 있다. 하지만 여기에서는 브라우저에서의 기능 테스트는 제외한 사용자 요청 자체를 시작점으로 잡았다. 브라우저의 기능 테스트도 가능하다고 한다. 프론트엔드의 테스트는 아래 피라미드 그림에 소개 링크를 걸어두었다.)
7.2. E2E 테스트 방법
7.2.1. 간단한 E2E 테스트 방법
상품, 주문 e2e 테스트
첫 번째 테스트 방법을 소개한다. 프로덕션 코드로 작성된 상품 컨트롤러를 주입 받아 직접적으로 컨트롤러의 메소드를 호출한다.
package simple.testcode.product.e2e;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import simple.testcode.common.controller.ApiResponse;
import simple.testcode.product.controller.ProductController;
import simple.testcode.product.domain.ProductSellingStatus;
import simple.testcode.product.dto.ProductRequest;
import simple.testcode.product.dto.ProductResponse;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Slf4j
public class ProductEndToEndTest {
@Autowired
ProductController productController;
@DisplayName("[상품 생성] 에 대한 E2E 테스트__응답 데이터 까지 확인")
@Test
void fromProductController() {
// given
ProductRequest request = ProductRequest.builder()
.sellingStatus(ProductSellingStatus.SELLING)
.name("콜드브루라떼")
.price(4000)
.build();
// when
ApiResponse<ProductResponse> response = productController.createProduct(request);
// then
// response code
assertThat(response.getCode()).isEqualTo(200);
// response data
assertThat(response.getData()).extracting("sellingStatus", "name", "price")
.contains(ProductSellingStatus.SELLING, "콜드브루라떼", 4000);
}
}
실제 서비스에 사용되는 ProductController 을 주입받아 그대로 사용한 것을 볼 수 있다.
서비스의 정상작동을 확인하기 위해 응답코드와 예상 응답 데이터, 2가지를 검증하였다.
주문 생성의 전제 조건인 상품 정보가 있어야 한다. @BeforeEach의 setProductData 에서 상품 정보를 먼저 생성한다.
package simple.testcode.order.e2e;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import simple.testcode.common.controller.ApiResponse;
import simple.testcode.order.controller.OrderController;
import simple.testcode.order.dto.OrderRequest;
import simple.testcode.order.dto.OrderResponse;
import simple.testcode.product.controller.ProductController;
import simple.testcode.product.domain.ProductEntity;
import simple.testcode.product.domain.ProductSellingStatus;
import simple.testcode.product.dto.ProductRequest;
import simple.testcode.product.dto.ProductResponse;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
public class OrderEndToEndTest {
@Autowired OrderController orderController;
@Autowired
ProductController productController;
ProductResponse productResponse;
ProductRequest productRequest;
String productNumber;
@BeforeEach
void setPRoductData() {
productRequest = ProductRequest.builder()
.sellingStatus(ProductSellingStatus.SELLING)
.name("카페라떼")
.price(3200)
.build();
ApiResponse<ProductResponse> product = productController.createProduct(productRequest);
productResponse = product.getData();
}
@DisplayName("[주문 생성] 에 대한 E2E 테스트__응답 데이터 까지 확인")
@Test
void fromOrderController() {
// given
OrderRequest request = OrderRequest.builder()
.productNumbers(List.of(productResponse.getProductNumber()))
.build();
ProductEntity entity = productRequest.toServiceVo().toEntity(productResponse.getProductNumber());
// when
ApiResponse<OrderResponse> response = orderController.createOrder(request);
ProductResponse lastProductResponse = response.getData().getProducts().get(0);
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK);
assertThat(response.getData().getTotalPrice()).isEqualTo(productRequest.getPrice());
assertThat(lastProductResponse).extracting("productNumber", "price", "name", "sellingStatus")
.contains(entity.getProductNumber(), entity.getPrice(), entity.getName(), entity.getSellingStatus());
}
}
여기에서도 서비스의 정상작동을 확인하기 위해 응답코드와 예상 응답 데이터, 2가지를 검증하였다.
참고로 AssertJ 에서 Object 검증 관한 예시를 찾아보았다. https://codingnconcepts.com/java/java-test-single-assert-multiple-properties/
샘플로 다루고 있는 카페 키오스크 로직이 단순한 구조이기 때문에 End to End 테스트도 비교적 단순하게 검증이 가능하였다.
7.2.2. 요청 헤더 정보가 필요한 경우
9ab91613 7. End to End 테스트 작성 - 2.2. 요청 헤더 데이터 활용한 테스트
지금까지 다룬 카페 키오스크 도메인에서는 인증 절차가 없이, 전달해야하는 파라미터 정보들을 직접 생성해서 전달하였다. 하지만 테스트 대상 서비스가 로그인과 같이 요청 데이터에 헤더 정보를 필요로 하는 경우도 있다. 헤더 정보와 같은 요청과 관련된 정보들은 어떻게 설정하고 전달할까? Mockito 라이브러리를 활용해 테스트 해보자.
receiveMethod 한 API 에서 쿠키, 세션, 헤더 정보 확인 모두 확인해보자.
반환값은 요청 헤더 정보를 확인하기 위해 요청받았던 헤더 정보를 반환값으로 설정한 테스트용 Controller로 작성하였다.
package msa.tc.headersample;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Slf4j
@RestController
@RequiredArgsConstructor
public class PlainController {
private final PlainService plainService;
@GetMapping("/api/v1/plain/check-header")
public String receiveMethod(HttpServletRequest httpServletRequest) {
Assert.notNull(httpServletRequest, "Request must not be null");
String authorization = httpServletRequest.getHeader("Authorization");
if (httpServletRequest.getCookies() != null) {
// cookieTest
Cookie cookies[] = httpServletRequest.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("hello".equals(cookie.getName())) {
// for cookie test
return cookie.getValue();
}
}
}
}
// sessionTest
HttpSession session = httpServletRequest.getSession();
if (session != null) {
System.out.println(session.getId());
Object sessionAttribute = session.getAttribute("hello-session");
if (sessionAttribute != null) {
return sessionAttribute.toString();
} else {
return plainService.doService(authorization);
}
} else if (authorization == null){
return "wrong";
}
// headerTest
return plainService.doService(authorization);
}
}
// headerTest 만 PlainService 에서 처리된 응답값을 반환하고 나머지 // cookieTest, // sessionTest 는 컨트롤러에서 응답 처리를 하였다.
서비스는 테스트용으로 간단하게 작성하였다.
package msa.tc.headersample;
import org.springframework.stereotype.Service;
@Service
public class PlainService {
String doService(String serviceType) {
String result = "this_is_" + serviceType + "_env";
if (serviceType.equals("DEV")) {
// to do something at dev
} else if (serviceType.equals("STG")) {
// to do something at stg
}
return result;
}
}
Mockito 의 mock()을 통해 HttpServletRequest 을 mocking 하고 Mockito의 when() 을 통해 요청시 전달할 헤더와 같은 데이터를 설정한다.
when(request의 XX이가 주어졌을 때).thenReturn(OO으로 리턴한다);는 형식으로 mocking 처리를 하여 요청 헤더 정보를 테스트 대상인 컨트롤러에 어떻게 보낼지 정의할 수 있다.
package msa.tc.headersample;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpSession;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@SpringBootTest
class PlainControllerTest {
@Autowired
PlainController plainController;
HttpServletRequest request;
@BeforeEach
public void init () {
request = mock(HttpServletRequest.class);
}
@DisplayName("headerTest")
@Test
void headerTest() {
// given
String auth = "DEV";
// mocking 처리를 하여 요청 헤더 정보를 테스트 대상인 컨트롤러에 어떻게 보낼지 정의 //
// Authorization 헤더 값에 "DEV" 설정
when(request.getHeader("Authorization")).thenReturn(auth);
// when
// 플레인 서비스에서 this_is_DEV_env 로 결과 받음
String result = plainController.receiveMethod(request);
// then
assertThat(result).isEqualTo("this_is_" + auth + "_env");
}
@DisplayName("cookieTest")
@Test
void cookieTest() {
// given
// mocking 처리를 하여 요청 헤더 정보를 테스트 대상인 컨트롤러에 어떻게 보낼지 정의 //
// hello 쿠키에 world 쿠키값 설정
String cookieValue = "world";
Cookie mockCookie = mock(Cookie.class);
when(mockCookie.getName()).thenReturn("hello");
when(mockCookie.getValue()).thenReturn(cookieValue);
when(request.getCookies()).thenReturn(new Cookie[]{mockCookie});
// when
String result = plainController.receiveMethod(request);
// then
assertThat(result).isSameAs(cookieValue);
}
@DisplayName("sessionTest")
@Test
void sessionTest() {
// given
String attributeNumValue = "world-session";
// mocking 처리를 하여 요청 헤더 정보를 테스트 대상인 컨트롤러에 어떻게 보낼지 정의 //
// 세션에 "hello-session" 키에 "world-session" 값 설정
HttpSession mockSession = new MockHttpSession();
mockSession.setAttribute("hello-session", attributeNumValue);
when(request.getSession()).thenReturn(mockSession);
// when
String result = plainController.receiveMethod(request);
// then
assertThat(result).isSameAs(attributeNumValue);
}
}
mocking 처리한 HttpServletRequest 를 실제 PlainController 에서도 요청데이터를 잘 처리하는 것을 알 수 있다.
마지막으로 요청 헤더 정보 테스트를 작성하며 많은 참고를 한 사이트이다. 혹시 MockMvc 를 사용한 Presentation 계층 테스트에서도 요청 헤더 정보가 필요 할 수 있다. 아래 소개한 사이트 혹은 코드를 참고하면 활용 가능할 것 같다.
mockMvc.perform(post("url")
.session(session) // 추가
.content(content)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
참조
- https://stackoverflow.com/questions/57102137/mockito-httpserveletrequest-return-mock-cookie-not-working-as-expectedhttps://shinsunyoung.tistory.com/70
'테스트코드 > 작성방법' 카테고리의 다른 글
6. Presentation Layer Test (1) | 2024.03.26 |
---|---|
5. Business Layer Test (1) | 2024.03.26 |
4. 외부 시스템과의 연계 테스트 (1) | 2024.03.26 |
3. Persistence Layer Test (0) | 2024.03.26 |
2. Layered Architecture (0) | 2024.03.26 |