Skip to content

Commit ff536c6

Browse files
committed
test: RankingServiceTest 클래스 추가 및 랭킹 서비스 기능 테스트 구현
1 parent 205e26f commit ff536c6

1 file changed

Lines changed: 234 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package com.loopers.domain.ranking;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.DisplayName;
5+
import org.junit.jupiter.api.Nested;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.mockito.Mock;
9+
import org.mockito.junit.jupiter.MockitoExtension;
10+
import org.springframework.data.redis.core.RedisTemplate;
11+
import org.springframework.data.redis.core.ZSetOperations;
12+
13+
import java.time.LocalDate;
14+
import java.util.LinkedHashSet;
15+
import java.util.List;
16+
import java.util.Set;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.ArgumentMatchers.*;
20+
import static org.mockito.Mockito.*;
21+
22+
@ExtendWith(MockitoExtension.class)
23+
class RankingServiceTest {
24+
25+
@Mock
26+
private RedisTemplate<String, String> redisTemplate;
27+
28+
@Mock
29+
private ZSetOperations<String, String> zSetOperations;
30+
31+
private RankingService rankingService;
32+
33+
@BeforeEach
34+
void setUp() {
35+
when(redisTemplate.opsForZSet()).thenReturn(zSetOperations);
36+
rankingService = new RankingService(redisTemplate);
37+
}
38+
39+
@Nested
40+
@DisplayName("getTopNWithScores")
41+
class GetTopNWithScores {
42+
43+
@Test
44+
@DisplayName("Top-N 랭킹을 점수와 함께 조회한다")
45+
void shouldReturnTopNWithScores() {
46+
// given
47+
LocalDate date = LocalDate.of(2025, 1, 15);
48+
String key = "ranking:all:20250115";
49+
50+
Set<ZSetOperations.TypedTuple<String>> tuples = new LinkedHashSet<>();
51+
tuples.add(createTuple("100", 10.5));
52+
tuples.add(createTuple("200", 8.3));
53+
tuples.add(createTuple("300", 5.1));
54+
55+
when(zSetOperations.reverseRangeWithScores(key, 0, 2)).thenReturn(tuples);
56+
57+
// when
58+
List<RankingEntry> entries = rankingService.getTopNWithScores(date, 3);
59+
60+
// then
61+
assertThat(entries).hasSize(3);
62+
assertThat(entries.get(0).productId()).isEqualTo(100L);
63+
assertThat(entries.get(0).score()).isEqualTo(10.5);
64+
assertThat(entries.get(1).productId()).isEqualTo(200L);
65+
assertThat(entries.get(2).productId()).isEqualTo(300L);
66+
}
67+
68+
@Test
69+
@DisplayName("데이터가 없으면 빈 리스트를 반환한다")
70+
void shouldReturnEmptyListWhenNoData() {
71+
// given
72+
LocalDate date = LocalDate.of(2025, 1, 15);
73+
74+
when(zSetOperations.reverseRangeWithScores(anyString(), anyLong(), anyLong()))
75+
.thenReturn(null);
76+
77+
// when
78+
List<RankingEntry> entries = rankingService.getTopNWithScores(date, 10);
79+
80+
// then
81+
assertThat(entries).isEmpty();
82+
}
83+
84+
@Test
85+
@DisplayName("Redis 예외 발생 시 빈 리스트를 반환한다")
86+
void shouldReturnEmptyListOnException() {
87+
// given
88+
LocalDate date = LocalDate.of(2025, 1, 15);
89+
90+
when(zSetOperations.reverseRangeWithScores(anyString(), anyLong(), anyLong()))
91+
.thenThrow(new RuntimeException("Redis error"));
92+
93+
// when
94+
List<RankingEntry> entries = rankingService.getTopNWithScores(date, 10);
95+
96+
// then
97+
assertThat(entries).isEmpty();
98+
}
99+
}
100+
101+
@Nested
102+
@DisplayName("getRankingPage")
103+
class GetRankingPage {
104+
105+
@Test
106+
@DisplayName("페이지네이션으로 랭킹을 조회한다")
107+
void shouldReturnPaginatedRanking() {
108+
// given
109+
LocalDate date = LocalDate.of(2025, 1, 15);
110+
String key = "ranking:all:20250115";
111+
int page = 1;
112+
int size = 10;
113+
114+
Set<ZSetOperations.TypedTuple<String>> tuples = new LinkedHashSet<>();
115+
tuples.add(createTuple("110", 4.5));
116+
tuples.add(createTuple("120", 4.2));
117+
118+
when(zSetOperations.reverseRangeWithScores(key, 10, 19)).thenReturn(tuples);
119+
120+
// when
121+
List<RankingEntry> entries = rankingService.getRankingPage(date, page, size);
122+
123+
// then
124+
assertThat(entries).hasSize(2);
125+
verify(zSetOperations).reverseRangeWithScores(key, 10, 19);
126+
}
127+
}
128+
129+
@Nested
130+
@DisplayName("getRank")
131+
class GetRank {
132+
133+
@Test
134+
@DisplayName("상품의 순위를 조회한다 (1-based)")
135+
void shouldReturnRankOneBased() {
136+
// given
137+
LocalDate date = LocalDate.of(2025, 1, 15);
138+
Long productId = 100L;
139+
140+
when(zSetOperations.reverseRank(anyString(), eq("100"))).thenReturn(0L);
141+
142+
// when
143+
Long rank = rankingService.getRank(productId, date);
144+
145+
// then
146+
assertThat(rank).isEqualTo(1L); // 0 -> 1 (1-based)
147+
}
148+
149+
@Test
150+
@DisplayName("랭킹에 없는 상품은 null을 반환한다")
151+
void shouldReturnNullWhenNotInRanking() {
152+
// given
153+
LocalDate date = LocalDate.of(2025, 1, 15);
154+
Long productId = 999L;
155+
156+
when(zSetOperations.reverseRank(anyString(), eq("999"))).thenReturn(null);
157+
158+
// when
159+
Long rank = rankingService.getRank(productId, date);
160+
161+
// then
162+
assertThat(rank).isNull();
163+
}
164+
}
165+
166+
@Nested
167+
@DisplayName("getScore")
168+
class GetScore {
169+
170+
@Test
171+
@DisplayName("상품의 점수를 조회한다")
172+
void shouldReturnScore() {
173+
// given
174+
LocalDate date = LocalDate.of(2025, 1, 15);
175+
Long productId = 100L;
176+
177+
when(zSetOperations.score(anyString(), eq("100"))).thenReturn(15.5);
178+
179+
// when
180+
Double score = rankingService.getScore(productId, date);
181+
182+
// then
183+
assertThat(score).isEqualTo(15.5);
184+
}
185+
}
186+
187+
@Nested
188+
@DisplayName("getRankingSize")
189+
class GetRankingSize {
190+
191+
@Test
192+
@DisplayName("랭킹에 진입한 상품 수를 조회한다")
193+
void shouldReturnRankingSize() {
194+
// given
195+
LocalDate date = LocalDate.of(2025, 1, 15);
196+
197+
when(zSetOperations.zCard(anyString())).thenReturn(150L);
198+
199+
// when
200+
Long size = rankingService.getRankingSize(date);
201+
202+
// then
203+
assertThat(size).isEqualTo(150L);
204+
}
205+
206+
@Test
207+
@DisplayName("키가 없으면 0을 반환한다")
208+
void shouldReturnZeroWhenKeyNotExists() {
209+
// given
210+
LocalDate date = LocalDate.of(2025, 1, 15);
211+
212+
when(zSetOperations.zCard(anyString())).thenReturn(null);
213+
214+
// when
215+
Long size = rankingService.getRankingSize(date);
216+
217+
// then
218+
assertThat(size).isEqualTo(0L);
219+
}
220+
}
221+
222+
private ZSetOperations.TypedTuple<String> createTuple(String value, Double score) {
223+
return new ZSetOperations.TypedTuple<>() {
224+
@Override
225+
public String getValue() { return value; }
226+
@Override
227+
public Double getScore() { return score; }
228+
@Override
229+
public int compareTo(ZSetOperations.TypedTuple<String> o) {
230+
return Double.compare(this.getScore(), o.getScore());
231+
}
232+
};
233+
}
234+
}

0 commit comments

Comments
 (0)