KATEKEITH / TIL_log

📚 Today I Learned.
0 stars 0 forks source link

세미나 스프링캠프 - 실무에서 적용하는 테스트 코드 작성 방법과 노하우 #32

Open KATEKEITH opened 1 year ago

KATEKEITH commented 1 year ago

신규 가맹점 등록 Flow

image

1차 시도 - HTTP Mock Server Test Code

입력받은 값을 그대로 영속화하는 코드 작성

class ShopRegisterationService(
 private val shopRepository: ShopRepository
){
    fun register(

  ){

  }
}
@Test
fun `Shop 등록 테스트 케이스`( ) {

}

PartnerClient로 HTTP 통신하여 데이터를 받아와서 영속화 코드 작성

class ShopRegisterationService(
   private val shopRepository: ShopRepository,
   private val partnerClient: PartnerClient
){
    fun register(

  ){
      val partner = partnerClient.getPartnerBy(brn)

  }
}
@Test
fun `가맹점 등록 Mock HTTP Test`( ) {

     // given

      mockServer
            .expect(
                    requestTo("http://localhost:8080/api/v1/partner/${brn}")
            )
            .andExpect(method(HttpMethod.GET))
            .andRespond(
                  withStatus(HttpStatus.OK)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(
                                  """
                                        {
                                            "brn": "${brn},
                                            "name": "${name}
                                        }
                                  """.trimIndent()
                        )

            )

        // when
}
KATEKEITH commented 1 year ago

2차 시도 - @MockBean을 주입받아 객체의 행위를 Mocking하여 해결해보자!

@MockBean
private lateinit var partnerClient: PartnerClient

@Test
fun `register mock bean test`( ) {

     // given

      given(partnerClient.getPartnerBy(brn))
      .willReturn()

        // when

       // then
}

Application Context를 N 번 초기화하는 문제가 발생한다 !!!

image

KATEKEITH commented 1 year ago

3차 시도 - ApplicationContext 이슈 해결 - 그냥 Bean으로 관리하자

class ShopRegistrationServiceMockBeanTest( 
      private val shopRegistrationService: ShopRegistrationService
) : TestSupport( ) { 

    @Bean
    private lateinit var partnerClient: PartnerClient

    @Test
    fun `register mock bean test`( ) {

         // given

          given(partnerClient.getPartnerBy(brn))
          .willReturn()

            // when

           // then
    }

}
@TestConfiguration
class ClientTestConfiguration {
      @Bean
      @Primary
      fun mockPartnerClient( ) = mock(PartnerClient::class.java)
}

테스트에서만 사용하기 위해 @TestConfiguration으로 지정해서 Mock 객체를 등록한다.

class ShopRegistrationServiceMockBeanTest( 
      private val shopRegistrationService: ShopRegistrationService,
      private val mockPartnerClient: PartnerClient
) : TestSupport( ) { 

    @BeforEach
    fun resetMock( ) {
          Mockito.restt(mockPartnerClient)
    }

    @Test
    fun `register mock bean test`( ) {

         // given

          given(partnerClient.getPartnerBy(brn))
          .willReturn(PartnerResponse(brn, name))

            // when

           // then
    }

}
KATEKEITH commented 1 year ago

4차 시도 - Multi Module에서는 어떻게? java-test-fixtures

image

KATEKEITH commented 1 year ago

image

KATEKEITH commented 1 year ago

BUT, HTTP Mock Test는 필요하다!!!

image

KATEKEITH commented 1 year ago

리팩토링 - Business Logic Layer, HTTP 통신 책임 분리

PartnerClient

class PartnerClient(
      private val restTemplate : RestTemplate
) {
      fun getPartnerBy(brn: String): PartnerResponse {
            return restTemplate
                  .getForObject(
                  "",
                  PartnerResponse::class.java
                  )!!
      }
}

PartnerClientService

@Service
class PartnerClientService(
private val partnerClient: PartnerClient
) {
      fun getPartnerBy(): PartnerResponse {
             val response = partnerClient.getPartnerByResponse(brn)
             if (response.statusCode.is2xxSuccessful.not()) {
                 throw IllegalArgumentException("...")
             }
             return response.body !!
      }
}

ShopRegistrationServiceMockBeanTest

@Import(ClientTestConfiguration::class)
class PartnerClientServiceTest(
    private val partnerClientServuce: PartnerClientService,
    private val : 
) : TestSupport() {

    @Test
    fun `getPartnerBy 200`() {
              // given
              given(partnerClient.getPartnerBy(brn))
              .willReturn(
                    ResponseEntity(
                            response, 
                            HttpStatus.OK
                    )
                )

                // when
              val result = partnerClientService.getPartnerBy(brn)

               // then
              then().isEqualTo()
              then().isEqualTo()
    }

}

@Import(ClientTestConfiguration::class)
class PartnerClientServiceTest(
   private val partnerClientServuce: PartnerClientService,
    private val : 
) : TestSupport() {

    @Test
    fun `getPartnerBy 400`() {
              // given
               given(partnerClient.getPartnerBy(brn))
              .willReturn(
                     ResponseEntity(
                              response, 
                              HttpStatus.BAD_REQUEST
                    )
                )

               // when
              thenThrownBy {
                      partnerClientService.getPartnerBy(brn)
              }
              .isInstanceOf(Exception::class.java)

               // then
    }

}
KATEKEITH commented 1 year ago

신규 가맹점 등록2

image

기존 코드는 2xx이 아닌 경우 Exception을 발생시키고 있어 코드 변경 필요

HTTP 통신 책임을 위임 했지만 객체의 요청/ 응답이 외부 라이브버리에 지나치게 의존

리팩토링 - ResponseEntity로 응답하여, 호출하는 곳에서 직접 제어

@Service
class PartnerClientService(
private val partnerClient: PartnerClient
) {
      fun getPartnerBy(): PartnerResponse {
             val response = partnerClient.getPartnerByResponse(brn)
             if (response.statusCode.is2xxSuccessful.not()) {
                 throw IllegalArgumentException("...")
             }
             return response.body !!
      }

      fun getPartner(brn: String): ResponseEntity<PartnerResponse> {
            return partnerClient.getPartnerByResponse(brn)
      }

}

리팩토링 - 특정 아리브러리 및 계층에 의존하지 않는 방향으로 개선: Pair<Int, T?> 응답으로 의존성 제거

@Service
class PartnerClientService(
private val partnerClient: PartnerClient
) {
      fun getPartnerBy(): PartnerResponse {
             val response = partnerClient.getPartnerByResponse(brn)
             if (response.statusCode.is2xxSuccessful.not()) {
                 throw IllegalArgumentException("...")
             }
             return response.body !!
      }

      fun getPartner(brn: String): ResponseEntity<PartnerResponse> {
            return partnerClient.getPartnerByResponse(brn)
      }

fun getPartner(brn: String): Pair<Int, PartnerResponse?> {
      val partnerByResponse = partnerClient.getPartnerByResponse(brn)
            return Pair(
                  first = partnerByResponse.statusCode.value(),
                  second = partnerByResponse.body
            )
      }

}
KATEKEITH commented 1 year ago

상품 주문 Flow

image

주문은 매우 복잡하고 중요한 Business Logic으로 다양한 케이스의 테스트 코드가 필요하다.

OrderService의 두 가지 책임

image

KATEKEITH commented 1 year ago

리팩토링 - 책임분리 : 인프라스트럭처에 의존하지 않는 POJO 객체를 새로운 협력 객체를 만들어 책임 분리

OrderServiceSupport

class OrderServiceSupport {

      fun order(
            product: Product,
            orderDate: LocalDate,

      ): Order {

            // 상품 정보 조회 하여 금액 및 상품 재고 확인, 재고가 없는 경우 예외 처리 등등

            // 환율 정보 조회 하여 특정 국가 환율로 계산

            // 쿠폰 정보 조회하여 적용 가능한 상품인지 확인, 가맹점과 할인, 가맹점과 할인 금액 부담 비율 등등 계산

            // 가맹점 정보 조회하여 수수료 정보등 조

            return Order()
      }

}

OrderService

fun order(

): String {

      val product = producrQueryService.findById(productId)

      val exchangeRateResponse =  exchangeRateClientImpl.getEachangeRate(orderDate, "USD", "KRW")

      val coupon = couponQueryService.findByCode(couponCode)

      val shop = shopQueryService.findById(shopId) 

      // 복잡한 로직은... OrderServiceSupport 객체로 위임
      val order = OrderServiceSupport().order(...)
      val order = save(order)
      return order.ordernumber

}

복잡한 로직의 책임은 위임하고 여러 인프라의 데이터 조회의 책임만 할당한다.

다양한 케이스보단 인프라 조회가 관심사이다.

KATEKEITH commented 1 year ago

image