Skip to content

Commit 5c03eed

Browse files
authored
feat(be) : 테스트 코드 수정 및 추가 (#123)
* fix(be) : Test코드 통과하도록 수정, Mock 테스트 추가 * feat(be) : service의 parsing 기능 테스트 추가 * feat(be) : 캐시 테스트 추가 및 MockK 의존성 추가 * refactor(be) : 실제 api 호출 테스트 제거
1 parent 87e25d3 commit 5c03eed

File tree

4 files changed

+243
-42
lines changed

4 files changed

+243
-42
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ dependencies {
7474
testImplementation("org.springframework.boot:spring-boot-starter-test")
7575
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
7676
testImplementation("org.springframework.security:spring-security-test")
77+
testImplementation("io.mockk:mockk:1.13.12")
7778
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
7879
}
7980

src/test/kotlin/com/back/koreaTravelGuide/domain/ai/tour/client/TourApiClientTest.kt

Lines changed: 111 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,136 @@ package com.back.koreaTravelGuide.domain.ai.tour.client
22

33
import com.back.koreaTravelGuide.KoreaTravelGuideApplication
44
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams
5-
import org.junit.jupiter.api.Assumptions.assumeTrue
5+
import com.fasterxml.jackson.databind.ObjectMapper
6+
import org.junit.jupiter.api.AfterEach
7+
import org.junit.jupiter.api.BeforeEach
68
import org.junit.jupiter.api.DisplayName
9+
import org.junit.jupiter.api.Nested
710
import org.junit.jupiter.api.Test
8-
import org.springframework.beans.factory.annotation.Autowired
9-
import org.springframework.beans.factory.annotation.Value
1011
import org.springframework.boot.test.context.SpringBootTest
12+
import org.springframework.http.HttpMethod
13+
import org.springframework.http.HttpStatus
14+
import org.springframework.http.MediaType
1115
import org.springframework.test.context.ActiveProfiles
16+
import org.springframework.test.web.client.ExpectedCount
17+
import org.springframework.test.web.client.MockRestServiceServer
18+
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
19+
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
20+
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
21+
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
22+
import org.springframework.web.client.RestTemplate
23+
import org.springframework.web.util.UriComponentsBuilder
24+
import kotlin.test.assertEquals
1225
import kotlin.test.assertTrue
1326

14-
/**
15-
* 실제 관광청 API 상태를 확인하기 위한 통합 테스트.
16-
*/
1727
@SpringBootTest(classes = [KoreaTravelGuideApplication::class])
1828
@ActiveProfiles("test")
1929
class TourApiClientTest {
20-
@Autowired
21-
private lateinit var tourApiClient: TourApiClient
30+
// MockRestServiceServer 기반 단위 테스트
31+
@Nested
32+
inner class MockServerTests {
33+
private lateinit var restTemplate: RestTemplate
34+
private lateinit var mockServer: MockRestServiceServer
35+
private lateinit var mockClient: TourApiClient
2236

23-
@Value("\${tour.api.key}")
24-
private lateinit var serviceKey: String
37+
private val serviceKey = "test-service-key"
38+
private val baseUrl = "https://example.com"
2539

26-
@DisplayName("fetchTourInfo - 실제 관광청 API 호출 (데이터 기대)")
27-
@Test
28-
fun fetchTourInfoTest() {
29-
val params =
30-
TourParams(
31-
contentTypeId = "12",
32-
areaCode = "1",
33-
sigunguCode = "1",
34-
)
40+
@BeforeEach
41+
fun setUp() {
42+
restTemplate = RestTemplate()
43+
mockServer = MockRestServiceServer.createServer(restTemplate)
44+
mockClient = TourApiClient(restTemplate, ObjectMapper(), serviceKey, baseUrl)
45+
}
46+
47+
@AfterEach
48+
fun tearDown() {
49+
mockServer.verify()
50+
}
3551

36-
val result = tourApiClient.fetchTourInfo(params)
52+
@DisplayName("fetchTourInfo - 외부 API가 정상 응답을 반환하면 파싱된 결과를 제공")
53+
@Test
54+
fun fetchTourInfoReturnsParsedItems() {
55+
val params =
56+
TourParams(
57+
contentTypeId = "12",
58+
areaCode = "1",
59+
sigunguCode = "1",
60+
)
3761

38-
println("실제 API 응답 아이템 수: ${result.items.size}")
39-
println("첫 번째 아이템: ${result.items.firstOrNull()}")
62+
mockServer.expect(ExpectedCount.once(), requestTo(expectedAreaBasedListUrl(params)))
63+
.andExpect(method(HttpMethod.GET))
64+
.andRespond(withSuccess(SUCCESS_RESPONSE, MediaType.APPLICATION_JSON))
4065

41-
assertTrue(result.items.isNotEmpty(), "실제 API 호출 결과가 비어 있습니다. 장애 여부를 확인하세요.")
42-
}
66+
val result = mockClient.fetchTourInfo(params)
4367

44-
@DisplayName("fetchTourInfo - 실제 관광청 API 장애 시 빈 결과 확인")
45-
@Test
46-
fun fetchTourInfoEmptyTest() {
47-
val params =
48-
TourParams(
49-
contentTypeId = "12",
50-
areaCode = "1",
51-
sigunguCode = "1",
52-
)
68+
assertEquals(1, result.items.size)
69+
val firstItem = result.items.first()
70+
assertEquals("12345", firstItem.contentId)
71+
assertEquals("테스트 타이틀", firstItem.title)
72+
}
73+
74+
@DisplayName("fetchTourInfo - 외부 API가 404를 반환하면 빈 결과를 전달")
75+
@Test
76+
fun fetchTourInfoReturnsEmptyListWhenApiFails() {
77+
val params =
78+
TourParams(
79+
contentTypeId = "12",
80+
areaCode = "1",
81+
sigunguCode = "1",
82+
)
5383

54-
val result = tourApiClient.fetchTourInfo(params)
84+
mockServer.expect(ExpectedCount.once(), requestTo(expectedAreaBasedListUrl(params)))
85+
.andExpect(method(HttpMethod.GET))
86+
.andRespond(withStatus(HttpStatus.NOT_FOUND))
5587

56-
println("실제 API 응답 아이템 수: ${result.items.size}")
57-
println("첫 번째 아이템: ${result.items.firstOrNull()}")
88+
val result = mockClient.fetchTourInfo(params)
5889

59-
// 장애가 아닐 경우, 테스트를 스킵
60-
assumeTrue(result.items.isEmpty()) {
61-
"API가 정상 응답을 반환하고 있어 장애 시나리오 테스트를 건너뜁니다."
90+
assertTrue(result.items.isEmpty())
6291
}
6392

64-
// 장애 상황일 시
65-
println("실제 API가 비어 있는 응답을 반환했습니다.")
66-
assertTrue(result.items.isEmpty())
93+
private fun expectedAreaBasedListUrl(params: TourParams): String =
94+
UriComponentsBuilder.fromUriString(baseUrl)
95+
.path("/areaBasedList2")
96+
.queryParam("serviceKey", serviceKey)
97+
.queryParam("MobileOS", "WEB")
98+
.queryParam("MobileApp", "KoreaTravelGuide")
99+
.queryParam("_type", "json")
100+
.queryParam("contentTypeId", params.contentTypeId)
101+
.queryParam("areaCode", params.areaCode)
102+
.queryParam("sigunguCode", params.sigunguCode)
103+
.build()
104+
.encode()
105+
.toUriString()
106+
}
107+
108+
companion object {
109+
private val SUCCESS_RESPONSE =
110+
"""
111+
{
112+
"response": {
113+
"header": {
114+
"resultCode": "0000",
115+
"resultMsg": "OK"
116+
},
117+
"body": {
118+
"items": {
119+
"item": [
120+
{
121+
"contentid": "12345",
122+
"contenttypeid": "12",
123+
"createdtime": "202310010000",
124+
"modifiedtime": "202310020000",
125+
"title": "테스트 타이틀",
126+
"addr1": "서울특별시 종로구",
127+
"areacode": "1",
128+
"firstimage": "https://example.com/image.jpg"
129+
}
130+
]
131+
}
132+
}
133+
}
134+
}
135+
""".trimIndent()
67136
}
68137
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.back.koreaTravelGuide.domain.ai.tour.service
2+
3+
import kotlin.test.assertEquals
4+
import kotlin.test.assertNull
5+
import org.junit.jupiter.api.DisplayName
6+
import org.junit.jupiter.api.Test
7+
8+
class TourParamsParserTest {
9+
private val parser = TourParamsParser()
10+
11+
@DisplayName("parse - 공백이 섞인 입력을 정리해 DTO를 만든다")
12+
@Test
13+
fun parseTrimsTokens() {
14+
val result = parser.parse("12", " 6 , 10 ")
15+
16+
assertEquals("12", result.contentTypeId)
17+
assertEquals("6", result.areaCode)
18+
assertEquals("10", result.sigunguCode)
19+
}
20+
21+
@DisplayName("parse - 시군구 코드가 없으면 null 로 남긴다")
22+
@Test
23+
fun parseWhenSigunguMissing() {
24+
val result = parser.parse("15", "7")
25+
26+
assertEquals("15", result.contentTypeId)
27+
assertEquals("7", result.areaCode)
28+
assertNull(result.sigunguCode)
29+
}
30+
31+
@DisplayName("parse - 콤마가 여러 번 등장하면 빈 문자열을 허용한다")
32+
@Test
33+
fun parseWhenCommaRepeated() {
34+
val result = parser.parse("32", "1,,2")
35+
36+
assertEquals("32", result.contentTypeId)
37+
assertEquals("1", result.areaCode)
38+
assertEquals("", result.sigunguCode)
39+
}
40+
41+
@DisplayName("parse - 완전히 비어 있는 입력은 빈 문자열과 null 로 파싱된다")
42+
@Test
43+
fun parseWhenInputBlank() {
44+
val result = parser.parse("25", "")
45+
46+
assertEquals("25", result.contentTypeId)
47+
assertEquals("", result.areaCode)
48+
assertNull(result.sigunguCode)
49+
}
50+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.back.koreaTravelGuide.domain.ai.tour.service.core
2+
3+
import com.back.koreaTravelGuide.domain.ai.tour.client.TourApiClient
4+
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourItem
5+
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams
6+
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse
7+
import com.back.koreaTravelGuide.domain.ai.tour.service.usecase.TourAreaBasedUseCase
8+
import io.mockk.every
9+
import io.mockk.mockk
10+
import io.mockk.verify
11+
import org.junit.jupiter.api.DisplayName
12+
import org.junit.jupiter.api.Test
13+
import org.springframework.beans.factory.annotation.Autowired
14+
import org.springframework.cache.CacheManager
15+
import org.springframework.cache.annotation.EnableCaching
16+
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
17+
import org.springframework.context.annotation.Bean
18+
import org.springframework.context.annotation.Configuration
19+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig
20+
import kotlin.test.assertEquals
21+
22+
@SpringJUnitConfig(TourAreaBasedServiceCoreCacheTest.Config::class)
23+
class TourAreaBasedServiceCoreCacheTest {
24+
@Autowired
25+
private lateinit var service: TourAreaBasedUseCase
26+
27+
@Autowired
28+
private lateinit var tourApiClient: TourApiClient
29+
30+
@DisplayName("fetchAreaBasedTours - 동일 파라미터 두 번 호출 시 API는 한 번만 호출된다")
31+
@Test
32+
fun cachesAreaBasedTours() {
33+
val params = TourParams(contentTypeId = "15", areaCode = "3", sigunguCode = "5")
34+
val apiResponse =
35+
TourResponse(
36+
items =
37+
listOf(
38+
TourItem(
39+
contentId = "88888",
40+
contentTypeId = "15",
41+
createdTime = "202401010000",
42+
modifiedTime = "202401020000",
43+
title = "캐시 검증 관광지",
44+
addr1 = "대전 어딘가",
45+
areaCode = "3",
46+
firstimage = null,
47+
firstimage2 = null,
48+
mapX = null,
49+
mapY = null,
50+
distance = null,
51+
mlevel = null,
52+
sigunguCode = "5",
53+
lDongRegnCd = null,
54+
lDongSignguCd = null,
55+
),
56+
),
57+
)
58+
59+
every { tourApiClient.fetchTourInfo(params) } returns apiResponse
60+
61+
val firstCall = service.fetchAreaBasedTours(params)
62+
val secondCall = service.fetchAreaBasedTours(params)
63+
64+
assertEquals(apiResponse, firstCall)
65+
assertEquals(apiResponse, secondCall)
66+
verify(exactly = 1) { tourApiClient.fetchTourInfo(params) }
67+
}
68+
69+
@Configuration
70+
@EnableCaching
71+
class Config {
72+
@Bean
73+
fun tourApiClient(): TourApiClient = mockk(relaxed = true)
74+
75+
@Bean
76+
fun cacheManager(): CacheManager = ConcurrentMapCacheManager("tourAreaBased", "tourLocationBased", "tourDetail")
77+
78+
@Bean
79+
fun tourAreaBasedServiceCore(tourApiClient: TourApiClient): TourAreaBasedServiceCore = TourAreaBasedServiceCore(tourApiClient)
80+
}
81+
}

0 commit comments

Comments
 (0)