@@ -2,30 +2,39 @@ package com.back.koreaTravelGuide.domain.ai.tour.client
22
33import com.back.koreaTravelGuide.KoreaTravelGuideApplication
44import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams
5+ import com.fasterxml.jackson.databind.ObjectMapper
6+ import org.junit.jupiter.api.AfterEach
57import org.junit.jupiter.api.Assumptions.assumeTrue
8+ import org.junit.jupiter.api.BeforeEach
69import org.junit.jupiter.api.DisplayName
10+ import org.junit.jupiter.api.Nested
711import org.junit.jupiter.api.Test
812import org.springframework.beans.factory.annotation.Autowired
9- import org.springframework.beans.factory.annotation.Value
1013import org.springframework.boot.test.context.SpringBootTest
14+ import org.springframework.http.HttpMethod
15+ import org.springframework.http.HttpStatus
16+ import org.springframework.http.MediaType
1117import org.springframework.test.context.ActiveProfiles
18+ import org.springframework.test.web.client.ExpectedCount
19+ import org.springframework.test.web.client.MockRestServiceServer
20+ import org.springframework.test.web.client.match.MockRestRequestMatchers.method
21+ import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
22+ import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
23+ import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
24+ import org.springframework.web.client.RestTemplate
25+ import org.springframework.web.util.UriComponentsBuilder
26+ import kotlin.test.assertEquals
1227import kotlin.test.assertTrue
1328
14- /* *
15- * 실제 관광청 API 상태를 확인하기 위한 통합 테스트.
16- */
29+ // 실제 API 호출 기반 단위 테스트
1730@SpringBootTest(classes = [KoreaTravelGuideApplication ::class ])
1831@ActiveProfiles(" test" )
19- class TourApiClientTest {
20- @Autowired
21- private lateinit var tourApiClient: TourApiClient
22-
23- @Value(" \$ {tour.api.key}" )
24- private lateinit var serviceKey: String
25-
26- @DisplayName(" fetchTourInfo - 실제 관광청 API 호출 (데이터 기대)" )
32+ class TourApiClientTest @Autowired constructor(
33+ private val tourApiClient : TourApiClient ,
34+ ) {
35+ @DisplayName(" fetchTourInfo - 실제 관광청 API가 빈 응답을 줄 경우" )
2736 @Test
28- fun fetchTourInfoTest () {
37+ fun fetchTourInfoRealCallEmptyResponse () {
2938 val params =
3039 TourParams (
3140 contentTypeId = " 12" ,
@@ -35,34 +44,120 @@ class TourApiClientTest {
3544
3645 val result = tourApiClient.fetchTourInfo(params)
3746
38- println (" 실제 API 응답 아이템 수: ${result.items.size} " )
39- println (" 첫 번째 아이템: ${result.items.firstOrNull()} " )
47+ // isEmpty가 true인 경우 테스트를 진행, 아닐 경우 메세지 출력
48+ assumeTrue(result.items.isEmpty()) {
49+ " 관광청 API가 정상 데이터를 제공하고 있어 장애 시나리오 테스트를 건너뜁니다."
50+ }
4051
41- assertTrue(result.items.isNotEmpty(), " 실제 API 호출 결과가 비어 있습니다. 장애 여부를 확인하세요. " )
52+ assertTrue(result.items.isEmpty() )
4253 }
4354
44- @DisplayName(" fetchTourInfo - 실제 관광청 API 장애 시 빈 결과 확인" )
45- @Test
46- fun fetchTourInfoEmptyTest () {
47- val params =
48- TourParams (
49- contentTypeId = " 12" ,
50- areaCode = " 1" ,
51- sigunguCode = " 1" ,
52- )
55+ // MockRestServiceServer 기반 단위 테스트
56+ @Nested
57+ inner class MockServerTests {
58+ private lateinit var restTemplate: RestTemplate
59+ private lateinit var mockServer: MockRestServiceServer
60+ private lateinit var mockClient: TourApiClient
5361
54- val result = tourApiClient.fetchTourInfo(params)
62+ private val serviceKey = " test-service-key"
63+ private val baseUrl = " https://example.com"
5564
56- println (" 실제 API 응답 아이템 수: ${result.items.size} " )
57- println (" 첫 번째 아이템: ${result.items.firstOrNull()} " )
65+ @BeforeEach
66+ fun setUp () {
67+ restTemplate = RestTemplate ()
68+ mockServer = MockRestServiceServer .createServer(restTemplate)
69+ mockClient = TourApiClient (restTemplate, ObjectMapper (), serviceKey, baseUrl)
70+ }
5871
59- // 장애가 아닐 경우, 테스트를 스킵
60- assumeTrue(result.items.isEmpty() ) {
61- " API가 정상 응답을 반환하고 있어 장애 시나리오 테스트를 건너뜁니다. "
72+ @AfterEach
73+ fun tearDown ( ) {
74+ mockServer.verify()
6275 }
6376
64- // 장애 상황일 시
65- println (" 실제 API가 비어 있는 응답을 반환했습니다." )
66- assertTrue(result.items.isEmpty())
77+ @DisplayName(" fetchTourInfo - 외부 API가 정상 응답을 반환하면 파싱된 결과를 제공" )
78+ @Test
79+ fun fetchTourInfoReturnsParsedItems () {
80+ val params =
81+ TourParams (
82+ contentTypeId = " 12" ,
83+ areaCode = " 1" ,
84+ sigunguCode = " 1" ,
85+ )
86+
87+ mockServer.expect(ExpectedCount .once(), requestTo(expectedAreaBasedListUrl(params)))
88+ .andExpect(method(HttpMethod .GET ))
89+ .andRespond(withSuccess(SUCCESS_RESPONSE , MediaType .APPLICATION_JSON ))
90+
91+ val result = mockClient.fetchTourInfo(params)
92+
93+ assertEquals(1 , result.items.size)
94+ val firstItem = result.items.first()
95+ assertEquals(" 12345" , firstItem.contentId)
96+ assertEquals(" 테스트 타이틀" , firstItem.title)
97+ }
98+
99+ @DisplayName(" fetchTourInfo - 외부 API가 404를 반환하면 빈 결과를 전달" )
100+ @Test
101+ fun fetchTourInfoReturnsEmptyListWhenApiFails () {
102+ val params =
103+ TourParams (
104+ contentTypeId = " 12" ,
105+ areaCode = " 1" ,
106+ sigunguCode = " 1" ,
107+ )
108+
109+ mockServer.expect(ExpectedCount .once(), requestTo(expectedAreaBasedListUrl(params)))
110+ .andExpect(method(HttpMethod .GET ))
111+ .andRespond(withStatus(HttpStatus .NOT_FOUND ))
112+
113+ val result = mockClient.fetchTourInfo(params)
114+
115+ assertTrue(result.items.isEmpty())
116+ }
117+
118+ private fun expectedAreaBasedListUrl (params : TourParams ): String =
119+ UriComponentsBuilder .fromUriString(baseUrl)
120+ .path(" /areaBasedList2" )
121+ .queryParam(" serviceKey" , serviceKey)
122+ .queryParam(" MobileOS" , " WEB" )
123+ .queryParam(" MobileApp" , " KoreaTravelGuide" )
124+ .queryParam(" _type" , " json" )
125+ .queryParam(" contentTypeId" , params.contentTypeId)
126+ .queryParam(" areaCode" , params.areaCode)
127+ .queryParam(" sigunguCode" , params.sigunguCode)
128+ .build()
129+ .encode()
130+ .toUriString()
131+
132+ }
133+
134+ companion object {
135+ private val SUCCESS_RESPONSE =
136+ """
137+ {
138+ "response": {
139+ "header": {
140+ "resultCode": "0000",
141+ "resultMsg": "OK"
142+ },
143+ "body": {
144+ "items": {
145+ "item": [
146+ {
147+ "contentid": "12345",
148+ "contenttypeid": "12",
149+ "createdtime": "202310010000",
150+ "modifiedtime": "202310020000",
151+ "title": "테스트 타이틀",
152+ "addr1": "서울특별시 종로구",
153+ "areacode": "1",
154+ "firstimage": "https://example.com/image.jpg"
155+ }
156+ ]
157+ }
158+ }
159+ }
160+ }
161+ """ .trimIndent()
67162 }
68163}
0 commit comments