Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,157 +1,75 @@
package com.back.koreaTravelGuide.domain.ai.tour.client

import com.back.koreaTravelGuide.KoreaTravelGuideApplication
import com.back.koreaTravelGuide.domain.ai.tour.dto.InternalData
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.BeforeEach
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourSearchParams
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.web.client.RestTemplateBuilder
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
import org.springframework.web.client.RestTemplate
import org.springframework.web.util.UriComponentsBuilder
import java.net.URI
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

// 09.26 양현준
@ExtendWith(SpringExtension::class)
// 패키지 경로에서 메인 설정을 찾지 못하는 오류를 해결하기 위해 애플리케이션 클래스를 명시.
/**
* 실제 관광청 API 상태를 확인하기 위한 통합 테스트.
*/
@SpringBootTest(classes = [KoreaTravelGuideApplication::class])
@ActiveProfiles("test")
class TourApiClientTest {
@Autowired private lateinit var restTemplateBuilder: RestTemplateBuilder

@Autowired private lateinit var objectMapper: ObjectMapper
@Autowired
private lateinit var tourApiClient: TourApiClient

@Value("\${tour.api.key}")
private lateinit var serviceKey: String

@Value("\${tour.api.base-url}")
private lateinit var apiUrl: String

private lateinit var restTemplate: RestTemplate
private lateinit var mockServer: MockRestServiceServer
private lateinit var tourApiClient: TourApiClient

// 테스트마다 클라이언트와 Mock 서버를 새로 구성해 호출 상태를 초기화.
@BeforeEach
fun setUp() {
restTemplate = restTemplateBuilder.build()
mockServer = MockRestServiceServer.createServer(restTemplate)
tourApiClient = TourApiClient(restTemplate, objectMapper, serviceKey, apiUrl)
}

@DisplayName("fetchTourInfo - 첫 번째 관광 정보를 반환하는지.")
@Test
fun testReturnsFirstTourInfo() {
val params = InternalData(numOfRows = 2, pageNo = 1, areaCode = "1", sigunguCode = "7")
expectTourRequest(params, responseWithItems(sampleTourItem()))

val result: TourResponse? = tourApiClient.fetchTourInfo(params)

mockServer.verify()
assertNotNull(result)
assertEquals("2591792", result.contentId)
assertEquals("개봉유수지 생태공원", result.title)
assertEquals("7", result.sigunguCode)
}

@DisplayName("fetchTourInfo - item 배열이 비어 있으면 null을 돌려주는지.")
@DisplayName("fetchTourInfo - 실제 관광청 API 호출 (데이터 기대)")
@Test
fun testReturnsNullWhenItemsMissing() {
val params = InternalData(numOfRows = 1, pageNo = 1, areaCode = "1", sigunguCode = "7")
expectTourRequest(params, responseWithItems())
fun fetchTourInfoTest() {

val params =
TourSearchParams(
numOfRows = 1,
pageNo = 1,
contentTypeId = "12",
areaCode = "1",
sigunguCode = "1",
)

val result = tourApiClient.fetchTourInfo(params)

mockServer.verify()
assertNull(result)
}
println("실제 API 응답 아이템 수: ${result.items.size}")
println("첫 번째 아이템: ${result.items.firstOrNull()}")

// 요청 URL과 응답 바디를 미리 세팅해 Mock 서버가 기대한 호출을 검증.
private fun expectTourRequest(
params: InternalData,
responseBody: String,
) {
mockServer.expect(requestTo(buildExpectedUrl(params)))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON))
assertTrue(result.items.isNotEmpty(), "실제 API 호출 결과가 비어 있습니다. 장애 여부를 확인하세요.")
}

// 실제 클라이언트가 조합하는 URL과 동일한 형태를 만들어 비교한다.
private fun buildExpectedUrl(params: InternalData): String =
UriComponentsBuilder.fromUri(URI.create(apiUrl))
.path("/areaBasedList2")
.queryParam("serviceKey", serviceKey)
.queryParam("MobileOS", "WEB")
.queryParam("MobileApp", "KoreaTravelGuide")
.queryParam("_type", "json")
.queryParam("numOfRows", params.numOfRows)
.queryParam("pageNo", params.pageNo)
.queryParam("contentTypeId", params.contentTypeId)
.queryParam("areaCode", params.areaCode)
.queryParam("sigunguCode", params.sigunguCode)
.build()
.encode()
.toUriString()

// Jackson을 활용해 테스트 응답 JSON을 손쉽게 조립한다.
private fun responseWithItems(vararg items: Map<String, String>): String {
val response =
mapOf(
"response" to
mapOf(
"header" to mapOf("resultCode" to "0000", "resultMsg" to "OK"),
"body" to
mapOf(
"items" to mapOf("item" to items.toList()),
),
),
@DisplayName("fetchTourInfo - 실제 관광청 API 장애 시 빈 결과 확인")
@Test
fun fetchTourInfoEmptyTest() {

val params =
TourSearchParams(
numOfRows = 1,
pageNo = 1,
contentTypeId = "12",
areaCode = "1",
sigunguCode = "1",
)

return objectMapper.writeValueAsString(response)
}
val result = tourApiClient.fetchTourInfo(params)

println("실제 API 응답 아이템 수: ${result.items.size}")
println("첫 번째 아이템: ${result.items.firstOrNull()}")

// 테스트용 샘플 관광지 정의한다.
private fun sampleTourItem(): Map<String, String> =
mapOf(
"contentid" to "2591792",
"contenttypeid" to "12",
"createdtime" to "20190313221125",
"modifiedtime" to "20250316162225",
"title" to "개봉유수지 생태공원",
"addr1" to "서울특별시 구로구 개봉동",
"areacode" to "1",
"firstimage" to "",
"firstimage2" to "",
"mapx" to "126.8632141714",
"mapy" to "37.4924524597",
"mlevel" to "6",
"sigungucode" to "7",
"lDongRegnCd" to "11",
"lDongSignguCd" to "530",
)
// 장애가 아닐 경우, 테스트를 스킵
assumeTrue(result.items.isEmpty()) {
"API가 정상 응답을 반환하고 있어 장애 시나리오 테스트를 건너뜁니다."
}

// 테스트에서 RestTemplateBuilder 빈을 보장해 컨텍스트 로딩 실패 해결.
@TestConfiguration
class TestConfig {
@Bean
fun restTemplateBuilder(): RestTemplateBuilder = RestTemplateBuilder()
// 장애 상황일 시
println("실제 API가 비어 있는 응답을 반환했습니다.")
assertTrue(result.items.isEmpty())
}
}