Mockito 도입
TEST 과정에서 고려해야 되는 부분이 발생되었습니다. 이렇게 매번 TEST 할때마다 Toss Server 로 결제 승인 API 를 호출 해서 TEST 해야 할까요? TEST 할때마다 Toss 로 API 호출 한다는 것은 시간적 부분 비용적인 부분을 고려를 안 할수가 없습니다. 하필 TEST 시 Toss 서버가 불안정한다면? TEST 할때마다 Toss 에 절대적인 의존성이 커지게 됩니다.
Toss 서버로 상호작용 하는 로직 부분만 Mocking 처리 하므로써 이러한 의존에 대한 문제를 해결 하고자 합니다.
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 31 32 33 @ActiveProfiles ("test" ) @SpringBootTest class PaymentConfirmApiServiceTest { @MockBean private TossPaymentClient tossPaymentClient; @Test @DisplayName ("상품 주문 하기 위한 Confirm 기능을 실행 합니다." ) void paymentConfirm ( ) { ...(생략)... this .createStubbingTossPostPayment (paymentKey, paymentCheckOutResponse.getOrderId (), totalAmount); ...(생략)... ...(생략)... } private void createStubbingTossPostPayment (String paymentKey, String orderId, String amount ) { String responsePostPaymentsBody = "{\"mId\":\"tvivarepublica\",\"lastTransactionKey\":\"F69384D0887558A85A78199D534FBBD5\",\"paymentKey\":\"" + paymentKey + "\",\"orderId\":\"" + orderId + "\",\"orderName\":\"상품A 그외\",\"taxExemptionAmount\":0,\"status\":\"DONE\",\"requestedAt\":\"2024-11-30T16:53:34+09:00\",\"approvedAt\":\"2024-11-30T16:53:57+09:00\",\"useEscrow\":false,\"cultureExpense\":false,\"card\":{\"issuerCode\":\"11\",\"acquirerCode\":\"11\",\"number\":\"52361232****606*\",\"installmentPlanMonths\":0,\"isInterestFree\":false,\"interestPayer\":null,\"approveNo\":\"00000000\",\"useCardPoint\":false,\"cardType\":\"신용\",\"ownerType\":\"개인\",\"acquireStatus\":\"READY\",\"amount\":" + amount + "},\"virtualAccount\":null,\"transfer\":null,\"mobilePhone\":null,\"giftCertificate\":null,\"cashReceipt\":null,\"cashReceipts\":null,\"discount\":null,\"cancels\":null,\"secret\":\"ps_GePWvyJnrKvelyyyB2pLVgLzN97E\",\"type\":\"NORMAL\",\"easyPay\":null,\"country\":\"KR\",\"failure\":null,\"isPartialCancelable\":true,\"receipt\":{\"url\":\"https://dashboard.tosspayments.com/receipt/redirection?transactionId=tviva20241130165334qVdY4&ref=PX\"},\"checkout\":{\"url\":\"https://api.tosspayments.com/v1/payments/tviva20241130165334qVdY4/checkout\"},\"currency\":\"KRW\",\"totalAmount\":" + amount + ",\"balanceAmount\":6000,\"suppliedAmount\":5455,\"vat\":545,\"taxFreeAmount\":0,\"method\":\"카드\",\"version\":\"2022-11-16\",\"metadata\":null}" ; when (tossPaymentClient.paymentConfirm ( any (String .class ), any (PaymentConfirmInPut .class ) )).thenReturn (ResponseEntity .ok (responsePostPaymentsBody)); } }
‘tossPaymentClient’ 객체를 Mocking 처리를 하고 thenReturn 을 통해 내가 원하는 응답을 지정 할 수 있습니다.
Test Double
Test Double
Dummy 아무 것도 하지 않는 깡통 객체
Fake 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체 (ex. FakeRepository)
Stub 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체 그 외에는 응답하지 않는다.
Spy Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체, 일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수 있다.
Mock 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체
Presentation Layer 테스트
Presentation Layer 테스트 경우는 다른 Later 테스트와 다르게 가장 먼저 진입되는 테스트 계층 이라고 보면 됩니다. 즉 비지니스 로직을 진입 하기 전 프론트로 부터 요청 받은 요청값들이 정상적으로 검증 하고 있는가 TEST 입니다.
Presentation Layer 계층 부분 즉 Controller 영역만 TEST 검증을 하게 되고 그외 Business Layer, Persistence Layer 계층들은 모두 Mocking 처리를 해서 진행 할 계획 입니다.
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 31 32 33 34 35 36 37 @WebMvcTest (controllers = { PaymentConfirmController .class }) class PaymentConfirmControllerTest { @Autowired protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; @MockBean private PaymentConfirmApiService paymentConfirmApiService; @Test @DisplayName ("결제 승인 confirm 처리를 하도록 합니다." ) void paymentConfirm () throws Exception { PaymentConfirmRequest request = PaymentConfirmRequest .of ("paymentKey" , "orderId" , "6000" ); mockMvc.perform ( post ("/api/payments/confirm" ) .content (objectMapper.writeValueAsString (request)) .contentType (MediaType .APPLICATION_JSON ) ) .andDo (print ()) .andExpect (status ().is2xxSuccessful ()) .andExpect (jsonPath ("$.code" ).value ("204" )) .andExpect (jsonPath ("$.status" ).value ("NO_CONTENT" )) .andExpect (jsonPath ("$.message" ).value ("NO_CONTENT" )) .andExpect (jsonPath ("$.data" ).isEmpty ()); ; } }
여기서 주목 해야 하는것은 앞써 이야기 한대로 Controller 영역만 TEST 검증을 하게 되고 그외 Business Layer, Persistence Layer 계층들은 모두 Mocking 처리 한다는 점 입니다.
1 2 @MockBean private PaymentConfirmApiService paymentConfirmApiService;
Controller 단에 있는 ‘PaymentConfirmController’ 부터 가장 먼저 진입 되는 ‘PaymentConfirmApiService’ 객체를 가짜 객체 통해 Mocking 하는 모습을 볼 수 있습니다.
CQRS CQRS 경우 기존 CURD 로직들을 읽기 (query) 와 쓰기 (command) 영역을 별도로 분리해서 관리하는 것 입니다. 보통의 서비스에서는 대부분 읽기 처리 부분이 대부분 이라고 보시면 됩니다.
읽기 (query) 와 쓰기 (command) 영역 분리하면서 책임을 분리해 서로 연관 관계를 없애서 나중에 크게 생각해 볼때 읽기 부분에 대한 부하가 많이 발생시 별로로 분리한 query 영역을 집중 관리를 하면 됩니다.
또 하나의 장점은 Spring 에서는 @Transaction 옵션에서 readOnly 를 통해 true, false 를 지정 할 수 있습니다.
1 @Transaction (readOnly = true )
읽기 (query) 영역에서는 해당 트랜잭션 readOnly true 로 관리 하므로 데이터베이스의 slave 영역으로만 접근 할 수 있도록 관리 할 수 있습니다.
반대로 쓰기 (command) 영역에서는 트랜잭션 readOnly false 로 관리 해서 데이버베이스 master 영역으로 접근 하도록 관리 할 수 있습니다.
추가적으로 장점이 있는데요. 트랜잭션 readOnly true 로 설정시 CUD 작업이 동작하지 않고, 스냅샷 저장, 변경 감지 (dirty check)의 작업을 수행하지 않아 성능이 향상됩니다.
TEST 환경 통합 하기 TEST 환경 통합 설명하기 앞써서 지금까지 우리가 작성한 TEST 코드를 모두 한꺼번에 실행 한번 해볼려고 합니다.
이미지를 보시면 우리가 작성한 단일 테스트, 통합 테스트가 모두 정상적으로 통과 되는 것을 확인 할 수 있습니다.
결과 로그에서 ‘:: Spring Boot ::’ 로 검색 한번 해보면 5개가 검색 나온것을 확인 할 수 있는데요. 즉 5번 Spring Boot 서버가 띄우고 있다는 것 입니다. 이 부분은 비용적인 부분에서 많은 비용이 발생 된다는 뜻 입니다.
왜 전체 TEST 하게 된다면 이렇게 여러번 Spring Boot 서버가 띄우게 되는 걸까요?
일단 로그를 보면 스프링 부트가 띄울때 어떤 영역부터 띄우는지 확인 할 필요가 있습니다.
Starting PaymentConfirmApiServiceTest ….
Starting PaymentControllerTest ….
Starting PaymentCheckOutApiServiceTest ….
Starting PaymentConfirmControllerTest ….
Starting PaymentTestingApplicationTests ….
우리가 TEST 코드를 작성시 각각의 TEST 코드를 실행 할때마다 스프링 부트 서버가 다시 띄우고 있다는 점 입니다. 스프링 부트에서는 각각의 TEST 가 실행 하는 시점에서 조금이라도 환경이 다르게 된다면 다시 스프링 부트 서버를 띄우게 됩니다.
동일한 환경 속에 스프링 부트 띄우는 횟수를 최소화 하기로 하겠습니다.
1 2 3 4 5 6 7 8 @ActiveProfiles ("test" ) @SpringBootTest public abstract class IntegrationTestSupport { @MockBean protected TossPaymentClient tossPaymentClient; }
우선 ‘IntegrationTestSupport’ 클래스를 생성 하겠습니다.
1 2 3 4 5 6 7 8 9 @ActiveProfiles ("test" ) @SpringBootTest class PaymentConfirmApiServiceTest { @MockBean private TossPaymentClient tossPaymentClient; ... 생략 ... }
PaymentConfirmApiServiceTest
PaymentCheckOutApiServiceTest
PaymentTestingApplicationTests
TEST 클래스를 확인 해보시면
1 2 @ActiveProfiles ("test" ) @SpringBootTest
해당 객체를 모두 이전에 생성한 ‘IntegrationTestSupport’ 로 옮기도록 합니다.
그런 다음 ‘@MockBean’ 으로 등록한 객체들도 모두 ‘IntegrationTestSupport’ 로 옮기도록 합니다.
TEST 작성한 객체는 ‘IntegrationTestSupport’ extend 하도록 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @WebMvcTest (controllers = { PaymentCheckoutController .class , PaymentConfirmController .class , PaymentController .class }) public abstract class ControllerTestSupport { @Autowired protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; @MockBean protected PaymentApiService paymentApiService; @MockBean protected PaymentConfirmApiService paymentConfirmApiService; @MockBean protected PaymentCheckOutApiService paymentCheckOutApiService; }
이번에는 Controller 단 TEST 전용 통합 ‘ControllerTestSupport’ 객체를 만들도록 합니다.
PaymentControllerTest
PaymentConfirmControllerTest
Controller 객체를 모두 동일하게 @WebMvcTest 에노테이션 코드를 ‘ControllerTestSupport’ 객체로 옮기도록 합니다.
TEST 작성한 Controller 객체는 ‘ControllerTestSupport’ extend 하도록 합니다.
이렇게 한 다음 다시 전체 TEST 를 하도록 하겠습니다.
이렇게 기존에 5번 스프링 부트가 반복적으로 띄워졌는데 이번에는 2번으로 줄였습니다.
Copyright 201- syh8088. 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.