Skip to content

Commit 376903f

Browse files
committed
test: 랭킹 시스템 테스트 추가
- RankingFacadeTest, ProductRankingCacheTest, RankingV1ApiE2ETest 추가 - MetricsAggregationServiceRankingTest, ProductRankingCacheTest 추가 - MetricsAggregationServiceIdempotencyTest에 MockBean 추가
1 parent bd05e11 commit 376903f

6 files changed

Lines changed: 936 additions & 0 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package com.loopers.application.ranking;
2+
3+
import com.loopers.application.ranking.RankingInfo.RankingItemInfo;
4+
import com.loopers.application.ranking.RankingInfo.RankingPageInfo;
5+
import com.loopers.domain.brand.Brand;
6+
import com.loopers.domain.brand.repository.BrandRepository;
7+
import com.loopers.domain.common.vo.Money;
8+
import com.loopers.domain.product.Product;
9+
import com.loopers.domain.product.repository.ProductRepository;
10+
import com.loopers.infrastructure.cache.ProductRankingCache;
11+
import com.loopers.infrastructure.cache.ProductRankingCache.RankingEntry;
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.ExtendWith;
15+
import org.mockito.InjectMocks;
16+
import org.mockito.Mock;
17+
import org.mockito.junit.jupiter.MockitoExtension;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.mockito.ArgumentMatchers.*;
24+
import static org.mockito.Mockito.*;
25+
26+
@ExtendWith(MockitoExtension.class)
27+
class RankingFacadeTest {
28+
29+
@Mock
30+
private ProductRankingCache productRankingCache;
31+
32+
@Mock
33+
private ProductRepository productRepository;
34+
35+
@Mock
36+
private BrandRepository brandRepository;
37+
38+
@InjectMocks
39+
private RankingFacade rankingFacade;
40+
41+
private static final String TODAY_DATE = "20251226";
42+
43+
@Test
44+
@DisplayName("getRankings - 상품 정보가 Aggregation 된다")
45+
void getRankings_aggregatesProductInfo() {
46+
// given
47+
List<RankingEntry> rankingEntries = List.of(
48+
new RankingEntry(1, 100L, 50.0),
49+
new RankingEntry(2, 200L, 40.0)
50+
);
51+
52+
Product product1 = createProduct(100L, "상품A", 1L, 10000);
53+
Product product2 = createProduct(200L, "상품B", 2L, 20000);
54+
Brand brand1 = createBrand(1L, "브랜드A");
55+
Brand brand2 = createBrand(2L, "브랜드B");
56+
57+
when(productRankingCache.getTodayDate()).thenReturn(TODAY_DATE);
58+
when(productRankingCache.getTopRankings(TODAY_DATE, 0, 10)).thenReturn(rankingEntries);
59+
when(productRankingCache.getTotalCount(TODAY_DATE)).thenReturn(2L);
60+
when(productRepository.findByIdIn(List.of(100L, 200L))).thenReturn(List.of(product1, product2));
61+
when(brandRepository.findByIdIn(List.of(1L, 2L))).thenReturn(List.of(brand1, brand2));
62+
63+
// when
64+
RankingPageInfo result = rankingFacade.getRankings(null, 0, 10);
65+
66+
// then
67+
assertThat(result.rankings()).hasSize(2);
68+
69+
RankingItemInfo first = result.rankings().get(0);
70+
assertThat(first.rank()).isEqualTo(1);
71+
assertThat(first.productId()).isEqualTo(100L);
72+
assertThat(first.productName()).isEqualTo("상품A");
73+
assertThat(first.brandName()).isEqualTo("브랜드A");
74+
assertThat(first.score()).isEqualTo(50.0);
75+
}
76+
77+
@Test
78+
@DisplayName("getRankings - 존재하지 않는 상품은 필터링된다")
79+
void getRankings_filtersNonExistentProducts() {
80+
// given
81+
List<RankingEntry> rankingEntries = List.of(
82+
new RankingEntry(1, 100L, 50.0),
83+
new RankingEntry(2, 999L, 40.0) // 존재하지 않는 상품
84+
);
85+
86+
Product product1 = createProduct(100L, "상품A", 1L, 10000);
87+
Brand brand1 = createBrand(1L, "브랜드A");
88+
89+
when(productRankingCache.getTodayDate()).thenReturn(TODAY_DATE);
90+
when(productRankingCache.getTopRankings(TODAY_DATE, 0, 10)).thenReturn(rankingEntries);
91+
when(productRankingCache.getTotalCount(TODAY_DATE)).thenReturn(2L);
92+
when(productRepository.findByIdIn(anyList())).thenReturn(List.of(product1));
93+
when(brandRepository.findByIdIn(anyList())).thenReturn(List.of(brand1));
94+
95+
// when
96+
RankingPageInfo result = rankingFacade.getRankings(null, 0, 10);
97+
98+
// then
99+
assertThat(result.rankings()).hasSize(1);
100+
assertThat(result.rankings().get(0).productId()).isEqualTo(100L);
101+
}
102+
103+
@Test
104+
@DisplayName("getRankings - 날짜 미지정 시 오늘 날짜 사용")
105+
void getRankings_usesTodayDateWhenNotSpecified() {
106+
// given
107+
when(productRankingCache.getTodayDate()).thenReturn(TODAY_DATE);
108+
when(productRankingCache.getTopRankings(TODAY_DATE, 0, 10)).thenReturn(Collections.emptyList());
109+
110+
// when
111+
RankingPageInfo result = rankingFacade.getRankings(null, 0, 10);
112+
113+
// then
114+
assertThat(result.date()).isEqualTo(TODAY_DATE);
115+
verify(productRankingCache).getTopRankings(eq(TODAY_DATE), anyInt(), anyInt());
116+
}
117+
118+
@Test
119+
@DisplayName("getRankings - 페이지 정보가 정확하다")
120+
void getRankings_pageInfoIsCorrect() {
121+
// given - 첫 페이지에 10개 상품이 있고, 전체 25개
122+
List<RankingEntry> rankingEntries = List.of(
123+
new RankingEntry(1, 100L, 50.0)
124+
);
125+
Product product = createProduct(100L, "상품A", 1L, 10000);
126+
Brand brand = createBrand(1L, "브랜드A");
127+
128+
when(productRankingCache.getTodayDate()).thenReturn(TODAY_DATE);
129+
when(productRankingCache.getTopRankings(TODAY_DATE, 0, 10)).thenReturn(rankingEntries);
130+
when(productRankingCache.getTotalCount(TODAY_DATE)).thenReturn(25L);
131+
when(productRepository.findByIdIn(anyList())).thenReturn(List.of(product));
132+
when(brandRepository.findByIdIn(anyList())).thenReturn(List.of(brand));
133+
134+
// when
135+
RankingPageInfo result = rankingFacade.getRankings(null, 0, 10);
136+
137+
// then
138+
assertThat(result.page()).isEqualTo(0);
139+
assertThat(result.size()).isEqualTo(10);
140+
assertThat(result.totalCount()).isEqualTo(25);
141+
assertThat(result.totalPages()).isEqualTo(3); // ceil(25/10)
142+
assertThat(result.hasNext()).isTrue();
143+
}
144+
145+
@Test
146+
@DisplayName("getRankings - 빈 결과 시 빈 리스트 반환")
147+
void getRankings_returnsEmptyListWhenNoResults() {
148+
// given
149+
when(productRankingCache.getTodayDate()).thenReturn(TODAY_DATE);
150+
when(productRankingCache.getTopRankings(TODAY_DATE, 0, 10)).thenReturn(Collections.emptyList());
151+
152+
// when
153+
RankingPageInfo result = rankingFacade.getRankings(null, 0, 10);
154+
155+
// then
156+
assertThat(result.rankings()).isEmpty();
157+
assertThat(result.totalCount()).isEqualTo(0);
158+
assertThat(result.hasNext()).isFalse();
159+
}
160+
161+
private Product createProduct(Long id, String name, Long brandId, int price) {
162+
Product product = mock(Product.class);
163+
when(product.getId()).thenReturn(id);
164+
when(product.getName()).thenReturn(name);
165+
when(product.getBrandId()).thenReturn(brandId);
166+
when(product.getPrice()).thenReturn(Money.of(price));
167+
when(product.getLikeCount()).thenReturn(10);
168+
return product;
169+
}
170+
171+
private Brand createBrand(Long id, String name) {
172+
Brand brand = mock(Brand.class);
173+
when(brand.getId()).thenReturn(id);
174+
when(brand.getName()).thenReturn(name);
175+
return brand;
176+
}
177+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package com.loopers.infrastructure.cache;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.DisplayName;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.ExtendWith;
7+
import org.mockito.Mock;
8+
import org.mockito.junit.jupiter.MockitoExtension;
9+
import org.springframework.data.redis.core.RedisTemplate;
10+
import org.springframework.data.redis.core.ZSetOperations;
11+
12+
import java.util.Collections;
13+
import java.util.LinkedHashSet;
14+
import java.util.List;
15+
import java.util.Set;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.mockito.ArgumentMatchers.*;
19+
import static org.mockito.Mockito.*;
20+
21+
@ExtendWith(MockitoExtension.class)
22+
class ProductRankingCacheTest {
23+
24+
@Mock
25+
private RedisTemplate<String, Object> redisTemplate;
26+
27+
@Mock
28+
private ZSetOperations<String, Object> zSetOperations;
29+
30+
private ProductRankingCache productRankingCache;
31+
32+
private static final Long PRODUCT_ID = 100L;
33+
private static final String TODAY_DATE = "20251226";
34+
35+
@BeforeEach
36+
void setUp() {
37+
productRankingCache = new ProductRankingCache(redisTemplate);
38+
when(redisTemplate.opsForZSet()).thenReturn(zSetOperations);
39+
}
40+
41+
@Test
42+
@DisplayName("getRank - 순위가 1-based로 반환된다")
43+
void getRank_returnsOneBasedRank() {
44+
// given
45+
when(zSetOperations.reverseRank(anyString(), eq(PRODUCT_ID.toString())))
46+
.thenReturn(0L); // 0-based rank
47+
48+
// when
49+
Integer rank = productRankingCache.getRank(PRODUCT_ID, TODAY_DATE);
50+
51+
// then
52+
assertThat(rank).isEqualTo(1); // 1-based
53+
}
54+
55+
@Test
56+
@DisplayName("getRank - 순위권 밖이면 null 반환")
57+
void getRank_returnsNullWhenNotRanked() {
58+
// given
59+
when(zSetOperations.reverseRank(anyString(), eq(PRODUCT_ID.toString())))
60+
.thenReturn(null);
61+
62+
// when
63+
Integer rank = productRankingCache.getRank(PRODUCT_ID, TODAY_DATE);
64+
65+
// then
66+
assertThat(rank).isNull();
67+
}
68+
69+
@Test
70+
@DisplayName("getScore - 점수가 정상 반환된다")
71+
void getScore_returnsScore() {
72+
// given
73+
when(zSetOperations.score(anyString(), eq(PRODUCT_ID.toString())))
74+
.thenReturn(15.5);
75+
76+
// when
77+
Double score = productRankingCache.getScore(PRODUCT_ID, TODAY_DATE);
78+
79+
// then
80+
assertThat(score).isEqualTo(15.5);
81+
}
82+
83+
@Test
84+
@DisplayName("getTopRankings - 페이지네이션이 동작한다")
85+
void getTopRankings_paginationWorks() {
86+
// given
87+
int page = 1;
88+
int size = 10;
89+
long expectedStart = 10L; // page * size
90+
long expectedEnd = 19L; // start + size - 1
91+
92+
Set<ZSetOperations.TypedTuple<Object>> mockResults = new LinkedHashSet<>();
93+
mockResults.add(createTuple("101", 50.0));
94+
mockResults.add(createTuple("102", 45.0));
95+
96+
when(zSetOperations.reverseRangeWithScores(anyString(), eq(expectedStart), eq(expectedEnd)))
97+
.thenReturn(mockResults);
98+
99+
// when
100+
List<ProductRankingCache.RankingEntry> rankings =
101+
productRankingCache.getTopRankings(TODAY_DATE, page, size);
102+
103+
// then
104+
assertThat(rankings).hasSize(2);
105+
assertThat(rankings.get(0).rank()).isEqualTo(11); // page * size + 1
106+
assertThat(rankings.get(0).productId()).isEqualTo(101L);
107+
assertThat(rankings.get(0).score()).isEqualTo(50.0);
108+
}
109+
110+
@Test
111+
@DisplayName("getTopRankings - 빈 결과 시 빈 리스트 반환")
112+
void getTopRankings_returnsEmptyListWhenNoResults() {
113+
// given
114+
when(zSetOperations.reverseRangeWithScores(anyString(), anyLong(), anyLong()))
115+
.thenReturn(Collections.emptySet());
116+
117+
// when
118+
List<ProductRankingCache.RankingEntry> rankings =
119+
productRankingCache.getTopRankings(TODAY_DATE, 0, 10);
120+
121+
// then
122+
assertThat(rankings).isEmpty();
123+
}
124+
125+
@Test
126+
@DisplayName("getTotalCount - 전체 개수를 반환한다")
127+
void getTotalCount_returnsTotalCount() {
128+
// given
129+
when(zSetOperations.zCard(anyString())).thenReturn(150L);
130+
131+
// when
132+
long count = productRankingCache.getTotalCount(TODAY_DATE);
133+
134+
// then
135+
assertThat(count).isEqualTo(150);
136+
}
137+
138+
@Test
139+
@DisplayName("getTotalCount - null일 경우 0 반환")
140+
void getTotalCount_returnsZeroWhenNull() {
141+
// given
142+
when(zSetOperations.zCard(anyString())).thenReturn(null);
143+
144+
// when
145+
long count = productRankingCache.getTotalCount(TODAY_DATE);
146+
147+
// then
148+
assertThat(count).isEqualTo(0);
149+
}
150+
151+
private ZSetOperations.TypedTuple<Object> createTuple(String value, Double score) {
152+
return new ZSetOperations.TypedTuple<>() {
153+
@Override
154+
public Object getValue() {
155+
return value;
156+
}
157+
158+
@Override
159+
public Double getScore() {
160+
return score;
161+
}
162+
163+
@Override
164+
public int compareTo(ZSetOperations.TypedTuple<Object> o) {
165+
return Double.compare(score, o.getScore());
166+
}
167+
};
168+
}
169+
}

0 commit comments

Comments
 (0)