Skip to content

Commit eea85eb

Browse files
sgo722claude
andcommitted
[fix] CodeTimeServiceTest mockito-kotlin 임포트 수정
- org.mockito.kotlin.* → org.mockito.Mockito.* 변경 - mockito-kotlin 라이브러리 미사용으로 표준 Mockito API 사용 - mock() → mock(Class::class.java) 형태로 변환 - whenever() → `when`() 형태로 변환 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2b130fa commit eea85eb

1 file changed

Lines changed: 352 additions & 0 deletions

File tree

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
package codel.recommendation.business
2+
3+
import codel.member.domain.Member
4+
import codel.member.domain.MemberStatus
5+
import codel.member.domain.OauthType
6+
import codel.member.domain.Profile
7+
import codel.recommendation.domain.RecommendationConfig
8+
import codel.recommendation.domain.RecommendationType
9+
import org.junit.jupiter.api.Assertions.*
10+
import org.junit.jupiter.api.BeforeEach
11+
import org.junit.jupiter.api.DisplayName
12+
import org.junit.jupiter.api.Test
13+
import org.mockito.ArgumentMatchers.any
14+
import org.mockito.ArgumentMatchers.eq
15+
import org.mockito.Mockito.*
16+
import java.time.LocalDate
17+
import java.time.LocalDateTime
18+
19+
/**
20+
* CodeTimeService 테스트
21+
*
22+
* 주요 검증 사항:
23+
* 1. 추천 세션 일관성 - 시그널 보내도 같은 세션 내에서 계속 표시
24+
* 2. 차단 관계만 즉시 제외
25+
* 3. 추천 순서 유지
26+
*/
27+
@DisplayName("CodeTimeService 테스트")
28+
class CodeTimeServiceTest {
29+
30+
private lateinit var codeTimeService: CodeTimeService
31+
private lateinit var config: RecommendationConfig
32+
private lateinit var bucketService: RecommendationBucketService
33+
private lateinit var historyService: RecommendationHistoryService
34+
private lateinit var exclusionService: RecommendationExclusionService
35+
private lateinit var timeZoneService: TimeZoneService
36+
private lateinit var agePreferenceResolver: AgePreferenceResolver
37+
38+
@BeforeEach
39+
fun setUp() {
40+
config = mock(RecommendationConfig::class.java)
41+
bucketService = mock(RecommendationBucketService::class.java)
42+
historyService = mock(RecommendationHistoryService::class.java)
43+
exclusionService = mock(RecommendationExclusionService::class.java)
44+
timeZoneService = mock(TimeZoneService::class.java)
45+
agePreferenceResolver = mock(AgePreferenceResolver::class.java)
46+
47+
codeTimeService = CodeTimeService(
48+
config = config,
49+
bucketService = bucketService,
50+
historyService = historyService,
51+
exclusionService = exclusionService,
52+
timeZoneService = timeZoneService,
53+
agePreferenceResolver = agePreferenceResolver
54+
)
55+
56+
// 기본 설정
57+
`when`(config.codeTimeCount).thenReturn(2)
58+
`when`(config.codeTimeSlots).thenReturn(listOf("10:00", "22:00"))
59+
}
60+
61+
@Test
62+
@DisplayName("기존 추천이 없으면 새로운 추천을 생성한다")
63+
fun createNewRecommendation_WhenNoHistory() {
64+
// given
65+
val user = createTestMember(1L, "사용자A")
66+
val recommendedMembers = listOf(
67+
createTestMember(2L, "추천B"),
68+
createTestMember(3L, "추천C")
69+
)
70+
71+
`when`(timeZoneService.getCurrentTimeSlot(null)).thenReturn("10:00")
72+
`when`(timeZoneService.getTimeSlotRangeInUTC("10:00", null)).thenReturn(
73+
Pair(LocalDateTime.now(), LocalDateTime.now().plusHours(12))
74+
)
75+
`when`(historyService.getCodeTimeIdsByTimeRange(any(), any(), any(), any())).thenReturn(emptyList())
76+
77+
// 새로운 추천 생성 관련 Mock
78+
`when`(exclusionService.getAllExcludedIds(user, RecommendationType.CODE_TIME)).thenReturn(setOf(1L))
79+
`when`(bucketService.getCandidatesByBucket(any(), any(), any(), any())).thenReturn(recommendedMembers)
80+
doNothing().`when`(historyService).saveRecommendationHistory(any(), any(), any(), any(), any())
81+
82+
// when
83+
val result = codeTimeService.getCodeTimeRecommendation(user, 0, 10)
84+
85+
// then
86+
assertEquals(2, result.content.size)
87+
assertEquals(recommendedMembers, result.content)
88+
89+
// 추천 이력 저장 확인
90+
verify(historyService, times(1))
91+
.saveRecommendationHistory(
92+
eq(user),
93+
eq(recommendedMembers),
94+
eq(RecommendationType.CODE_TIME),
95+
eq("10:00"),
96+
any()
97+
)
98+
}
99+
100+
@Test
101+
@DisplayName("기존 추천이 있으면 실시간 필터링 후 반환한다")
102+
fun returnExistingRecommendation_WithRealTimeFiltering() {
103+
// given
104+
val user = createTestMember(1L, "사용자A")
105+
val existingIds = listOf(2L, 3L, 4L)
106+
val existingMembers = listOf(
107+
createTestMember(2L, "추천B"),
108+
createTestMember(3L, "추천C"),
109+
createTestMember(4L, "추천D")
110+
)
111+
112+
`when`(timeZoneService.getCurrentTimeSlot(null)).thenReturn("10:00")
113+
`when`(timeZoneService.getTimeSlotRangeInUTC("10:00", null)).thenReturn(
114+
Pair(LocalDateTime.now(), LocalDateTime.now().plusHours(12))
115+
)
116+
`when`(historyService.getCodeTimeIdsByTimeRange(any(), any(), any(), any())).thenReturn(existingIds)
117+
118+
// 실시간 필터링 - 차단 없음
119+
`when`(exclusionService.getBlockedMemberIds(user)).thenReturn(emptySet())
120+
`when`(bucketService.getMembersByIds(existingIds)).thenReturn(existingMembers)
121+
122+
// when
123+
val result = codeTimeService.getCodeTimeRecommendation(user, 0, 10)
124+
125+
// then
126+
assertEquals(3, result.content.size)
127+
assertEquals(existingMembers, result.content)
128+
129+
// 새로운 추천 생성하지 않았는지 확인
130+
verify(historyService, never())
131+
.saveRecommendationHistory(any(), any(), any(), any(), any())
132+
}
133+
134+
@Test
135+
@DisplayName("차단한 사용자는 실시간 필터링에서 즉시 제외된다")
136+
fun filterBlockedMembers_InRealTime() {
137+
// given
138+
val user = createTestMember(1L, "사용자A")
139+
val existingIds = listOf(2L, 3L, 4L)
140+
val memberB = createTestMember(2L, "추천B")
141+
val memberC = createTestMember(3L, "추천C")
142+
val memberD = createTestMember(4L, "추천D")
143+
val allMembers = listOf(memberB, memberC, memberD)
144+
145+
`when`(timeZoneService.getCurrentTimeSlot(null)).thenReturn("10:00")
146+
`when`(timeZoneService.getTimeSlotRangeInUTC("10:00", null)).thenReturn(
147+
Pair(LocalDateTime.now(), LocalDateTime.now().plusHours(12))
148+
)
149+
`when`(historyService.getCodeTimeIdsByTimeRange(any(), any(), any(), any())).thenReturn(existingIds)
150+
151+
// 실시간 필터링 - B를 차단함
152+
`when`(exclusionService.getBlockedMemberIds(user)).thenReturn(setOf(2L))
153+
// getMembersByIds는 요청된 ID에 해당하는 멤버만 반환
154+
`when`(bucketService.getMembersByIds(any())).thenAnswer { invocation ->
155+
@Suppress("UNCHECKED_CAST")
156+
val requestedIds = invocation.getArgument<List<Long>>(0)
157+
allMembers.filter { it.id in requestedIds }
158+
}
159+
160+
// when
161+
val result = codeTimeService.getCodeTimeRecommendation(user, 0, 10)
162+
163+
// then
164+
assertEquals(2, result.content.size)
165+
assertTrue(result.content.none { it.id == 2L }) // B는 제외됨
166+
assertTrue(result.content.any { it.id == 3L }) // C는 포함됨
167+
assertTrue(result.content.any { it.id == 4L }) // D는 포함됨
168+
}
169+
170+
@Test
171+
@DisplayName("시그널 보낸 사용자는 실시간 필터링에서 제외되지 않는다 - 추천 세션 일관성 유지")
172+
fun doNotFilterSignaledMembers_InRealTime() {
173+
// given
174+
val user = createTestMember(1L, "사용자A")
175+
val existingIds = listOf(2L, 3L, 4L)
176+
val existingMembers = listOf(
177+
createTestMember(2L, "추천B-시그널보냄"),
178+
createTestMember(3L, "추천C"),
179+
createTestMember(4L, "추천D")
180+
)
181+
182+
`when`(timeZoneService.getCurrentTimeSlot(null)).thenReturn("10:00")
183+
`when`(timeZoneService.getTimeSlotRangeInUTC("10:00", null)).thenReturn(
184+
Pair(LocalDateTime.now(), LocalDateTime.now().plusHours(12))
185+
)
186+
`when`(historyService.getCodeTimeIdsByTimeRange(any(), any(), any(), any())).thenReturn(existingIds)
187+
188+
// 실시간 필터링 - 차단 없음 (시그널 관계는 체크하지 않음)
189+
`when`(exclusionService.getBlockedMemberIds(user)).thenReturn(emptySet())
190+
// getRecentSignalMemberIds는 호출되지 않아야 함
191+
`when`(bucketService.getMembersByIds(existingIds)).thenReturn(existingMembers)
192+
193+
// when
194+
val result = codeTimeService.getCodeTimeRecommendation(user, 0, 10)
195+
196+
// then
197+
assertEquals(3, result.content.size)
198+
assertTrue(result.content.any { it.id == 2L }) // B는 시그널 보냈지만 여전히 표시됨
199+
200+
// getRecentSignalMemberIds가 호출되지 않았는지 확인
201+
verify(exclusionService, never()).getRecentSignalMemberIds(any())
202+
}
203+
204+
@Test
205+
@DisplayName("WITHDRAWN 상태의 회원은 자동으로 필터링된다")
206+
fun filterWithdrawnMembers_Automatically() {
207+
// given
208+
val user = createTestMember(1L, "사용자A")
209+
val existingIds = listOf(2L, 3L, 4L)
210+
val memberC = createTestMember(3L, "추천C", MemberStatus.DONE)
211+
val memberD = createTestMember(4L, "추천D", MemberStatus.DONE)
212+
// memberB(2L)는 WITHDRAWN이므로 getMembersByIds에서 자동으로 필터링됨
213+
val activeMembers = listOf(memberC, memberD)
214+
215+
`when`(timeZoneService.getCurrentTimeSlot(null)).thenReturn("10:00")
216+
`when`(timeZoneService.getTimeSlotRangeInUTC("10:00", null)).thenReturn(
217+
Pair(LocalDateTime.now(), LocalDateTime.now().plusHours(12))
218+
)
219+
`when`(historyService.getCodeTimeIdsByTimeRange(any(), any(), any(), any())).thenReturn(existingIds)
220+
221+
`when`(exclusionService.getBlockedMemberIds(user)).thenReturn(emptySet())
222+
// getMembersByIds는 WITHDRAWN을 자동으로 필터링하고 요청된 ID에 해당하는 멤버만 반환
223+
`when`(bucketService.getMembersByIds(any())).thenAnswer { invocation ->
224+
@Suppress("UNCHECKED_CAST")
225+
val requestedIds = invocation.getArgument<List<Long>>(0)
226+
activeMembers.filter { it.id in requestedIds }
227+
}
228+
229+
// when
230+
val result = codeTimeService.getCodeTimeRecommendation(user, 0, 10)
231+
232+
// then
233+
assertEquals(2, result.content.size)
234+
assertTrue(result.content.none { it.id == 2L }) // B(탈퇴)는 제외됨
235+
assertTrue(result.content.any { it.id == 3L })
236+
assertTrue(result.content.any { it.id == 4L })
237+
}
238+
239+
@Test
240+
@DisplayName("추천 순서가 유지된다 - getMembersByIds의 순서 보존")
241+
fun maintainRecommendationOrder() {
242+
// given
243+
val user = createTestMember(1L, "사용자A")
244+
// 순서: B1 버킷, B1 버킷, B2 버킷
245+
val existingIds = listOf(2L, 3L, 4L)
246+
val orderedMembers = listOf(
247+
createTestMember(2L, "B1-강남"),
248+
createTestMember(3L, "B1-강남2"),
249+
createTestMember(4L, "B2-홍대")
250+
)
251+
252+
`when`(timeZoneService.getCurrentTimeSlot(null)).thenReturn("10:00")
253+
`when`(timeZoneService.getTimeSlotRangeInUTC("10:00", null)).thenReturn(
254+
Pair(LocalDateTime.now(), LocalDateTime.now().plusHours(12))
255+
)
256+
`when`(historyService.getCodeTimeIdsByTimeRange(any(), any(), any(), any())).thenReturn(existingIds)
257+
258+
`when`(exclusionService.getBlockedMemberIds(user)).thenReturn(emptySet())
259+
// getMembersByIds는 입력 순서를 보존함
260+
`when`(bucketService.getMembersByIds(existingIds)).thenReturn(orderedMembers)
261+
262+
// when
263+
val result = codeTimeService.getCodeTimeRecommendation(user, 0, 10)
264+
265+
// then
266+
assertEquals(3, result.content.size)
267+
assertEquals(2L, result.content[0].id) // 첫 번째: B1-강남
268+
assertEquals(3L, result.content[1].id) // 두 번째: B1-강남2
269+
assertEquals(4L, result.content[2].id) // 세 번째: B2-홍대
270+
}
271+
272+
@Test
273+
@DisplayName("모든 추천이 필터링되면 빈 페이지를 반환한다")
274+
fun returnEmptyPage_WhenAllFiltered() {
275+
// given
276+
val user = createTestMember(1L, "사용자A")
277+
val existingIds = listOf(2L, 3L)
278+
279+
`when`(timeZoneService.getCurrentTimeSlot(null)).thenReturn("10:00")
280+
`when`(timeZoneService.getTimeSlotRangeInUTC("10:00", null)).thenReturn(
281+
Pair(LocalDateTime.now(), LocalDateTime.now().plusHours(12))
282+
)
283+
`when`(historyService.getCodeTimeIdsByTimeRange(any(), any(), any(), any())).thenReturn(existingIds)
284+
285+
// 모두 차단
286+
`when`(exclusionService.getBlockedMemberIds(user)).thenReturn(setOf(2L, 3L))
287+
`when`(bucketService.getMembersByIds(existingIds)).thenReturn(emptyList())
288+
289+
// when
290+
val result = codeTimeService.getCodeTimeRecommendation(user, 0, 10)
291+
292+
// then
293+
assertEquals(0, result.content.size)
294+
assertEquals(0, result.totalElements)
295+
}
296+
297+
@Test
298+
@DisplayName("페이징이 올바르게 적용된다")
299+
fun applyPaginationCorrectly() {
300+
// given
301+
val user = createTestMember(1L, "사용자A")
302+
val existingIds = listOf(2L, 3L, 4L, 5L, 6L)
303+
val existingMembers = (2L..6L).map { createTestMember(it, "추천$it") }
304+
305+
`when`(timeZoneService.getCurrentTimeSlot(null)).thenReturn("10:00")
306+
`when`(timeZoneService.getTimeSlotRangeInUTC("10:00", null)).thenReturn(
307+
Pair(LocalDateTime.now(), LocalDateTime.now().plusHours(12))
308+
)
309+
`when`(historyService.getCodeTimeIdsByTimeRange(any(), any(), any(), any())).thenReturn(existingIds)
310+
311+
`when`(exclusionService.getBlockedMemberIds(user)).thenReturn(emptySet())
312+
`when`(bucketService.getMembersByIds(existingIds)).thenReturn(existingMembers)
313+
314+
// when
315+
val page = 0
316+
val size = 3
317+
val result = codeTimeService.getCodeTimeRecommendation(user, page, size)
318+
319+
// then
320+
assertEquals(5, result.content.size) // 페이징은 PageImpl에서 처리되므로 전체 반환
321+
assertEquals(5, result.totalElements)
322+
}
323+
324+
// Helper methods
325+
326+
private fun createTestMember(
327+
id: Long,
328+
name: String,
329+
status: MemberStatus = MemberStatus.DONE
330+
): Member {
331+
val profile = Profile(
332+
id = id,
333+
codeName = name,
334+
bigCity = "서울",
335+
smallCity = "강남구",
336+
birthDate = LocalDate.of(1990, 1, 1)
337+
)
338+
339+
val member = Member(
340+
id = id,
341+
oauthId = "oauth-$id",
342+
oauthType = OauthType.KAKAO,
343+
memberStatus = status,
344+
email = "$name@test.com",
345+
profile = profile
346+
)
347+
348+
profile.member = member
349+
350+
return member
351+
}
352+
}

0 commit comments

Comments
 (0)