11import { Pool } from 'pg' ;
22import { DBError } from '@/exception' ;
33import { LeaderboardRepository } from '@/repositories/leaderboard.repository' ;
4- import { LeaderboardService } from '@/services/leaderboard.service' ;
4+ import { LEADERBOARD_CACHE_TTL , LeaderboardService } from '@/services/leaderboard.service' ;
5+ import { ICache } from '@/modules/cache/cache.type' ;
56
67jest . mock ( '@/repositories/leaderboard.repository' ) ;
8+ jest . mock ( '@/configs/cache.config' , ( ) => ( {
9+ cache : {
10+ get : jest . fn ( ) ,
11+ set : jest . fn ( ) ,
12+ } ,
13+ } ) ) ;
714
815describe ( 'LeaderboardService' , ( ) => {
916 let service : LeaderboardService ;
10- let repo : jest . Mocked < LeaderboardRepository > ;
17+ let mockRepo : jest . Mocked < LeaderboardRepository > ;
1118 let mockPool : jest . Mocked < Pool > ;
19+ let mockCache : jest . Mocked < ICache > ;
1220
1321 beforeEach ( ( ) => {
1422 const mockPoolObj = { } ;
1523 mockPool = mockPoolObj as jest . Mocked < Pool > ;
1624
1725 const repoInstance = new LeaderboardRepository ( mockPool ) ;
18- repo = repoInstance as jest . Mocked < LeaderboardRepository > ;
26+ mockRepo = repoInstance as jest . Mocked < LeaderboardRepository > ;
1927
20- service = new LeaderboardService ( repo ) ;
28+ const { cache } = jest . requireMock ( '@/configs/cache.config' ) ;
29+ mockCache = cache as jest . Mocked < ICache > ;
30+
31+ service = new LeaderboardService ( mockRepo ) ;
2132 } ) ;
2233
2334 afterEach ( ( ) => {
2435 jest . clearAllMocks ( ) ;
2536 } ) ;
2637
2738 describe ( 'getUserLeaderboard' , ( ) => {
28- it ( '응답 형식에 맞게 변환된 사용자 리더보드 데이터를 반환해야 한다' , async ( ) => {
29- const mockRawResult = [
39+ const mockRawResult = [
40+ {
41+ id : '1' ,
42+ email : 'test@test.com' ,
43+ username : 'test' ,
44+ total_views : '100' ,
45+ total_likes : '50' ,
46+ total_posts : '1' ,
47+ view_diff : '20' ,
48+ like_diff : '10' ,
49+ post_diff : '1' ,
50+ } ,
51+ {
52+ id : '2' ,
53+ email : 'test2@test.com' ,
54+ username : 'test2' ,
55+ total_views : '200' ,
56+ total_likes : '100' ,
57+ total_posts : '2' ,
58+ view_diff : '10' ,
59+ like_diff : '5' ,
60+ post_diff : '1' ,
61+ } ,
62+ ] ;
63+
64+ const mockResult = {
65+ users : [
3066 {
3167 id : '1' ,
3268 email : 'test@test.com' ,
3369 username : 'test' ,
34- total_views : ' 100' ,
35- total_likes : '50' ,
36- total_posts : '1' ,
37- view_diff : '20' ,
38- like_diff : '10' ,
39- post_diff : '1' ,
70+ totalViews : 100 ,
71+ totalLikes : 50 ,
72+ totalPosts : 1 ,
73+ viewDiff : 20 ,
74+ likeDiff : 10 ,
75+ postDiff : 1 ,
4076 } ,
4177 {
4278 id : '2' ,
4379 email : 'test2@test.com' ,
4480 username : 'test2' ,
45- total_views : ' 200' ,
46- total_likes : ' 100' ,
47- total_posts : '2' ,
48- view_diff : '10' ,
49- like_diff : '5' ,
50- post_diff : '1' ,
81+ totalViews : 200 ,
82+ totalLikes : 100 ,
83+ totalPosts : 2 ,
84+ viewDiff : 10 ,
85+ likeDiff : 5 ,
86+ postDiff : 1 ,
5187 } ,
52- ] ;
53-
54- const mockResult = {
55- users : [
56- {
57- id : '1' ,
58- email : 'test@test.com' ,
59- username : 'test' ,
60- totalViews : 100 ,
61- totalLikes : 50 ,
62- totalPosts : 1 ,
63- viewDiff : 20 ,
64- likeDiff : 10 ,
65- postDiff : 1 ,
66- } ,
67- {
68- id : '2' ,
69- email : 'test2@test.com' ,
70- username : 'test2' ,
71- totalViews : 200 ,
72- totalLikes : 100 ,
73- totalPosts : 2 ,
74- viewDiff : 10 ,
75- likeDiff : 5 ,
76- postDiff : 1 ,
77- } ,
78- ] ,
79- } ;
80-
81- repo . getUserLeaderboard . mockResolvedValue ( mockRawResult ) ;
88+ ] ,
89+ } ;
90+
91+ beforeEach ( ( ) => {
92+ jest . clearAllMocks ( ) ;
93+ } ) ;
94+
95+ it ( '응답 형식에 맞게 변환된 사용자 리더보드 데이터를 반환해야 한다' , async ( ) => {
96+ mockCache . get . mockResolvedValue ( null ) ;
97+ mockRepo . getUserLeaderboard . mockResolvedValue ( mockRawResult ) ;
98+
8299 const result = await service . getUserLeaderboard ( 'viewCount' , 30 , 10 ) ;
83100
84101 expect ( result . users ) . toEqual ( mockResult . users ) ;
85102 } ) ;
86103
87104 it ( '쿼리 파라미터가 올바르게 적용되어야 한다' , async ( ) => {
88- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
105+ mockCache . get . mockResolvedValue ( null ) ;
106+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
89107
90108 await service . getUserLeaderboard ( 'postCount' , 30 , 10 ) ;
91109
92- expect ( repo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'postCount' , 30 , 10 ) ;
110+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'postCount' , 30 , 10 ) ;
93111 } ) ;
94112
95113 it ( '쿼리 파라미터가 입력되지 않은 경우 기본값으로 처리되어야 한다' , async ( ) => {
96- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
114+ mockCache . get . mockResolvedValue ( null ) ;
115+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
97116
98117 await service . getUserLeaderboard ( ) ;
99118
100- expect ( repo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
119+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
101120 } ) ;
102121
103122 it ( '데이터가 없는 경우 빈 배열을 반환해야 한다' , async ( ) => {
104- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
123+ mockCache . get . mockResolvedValue ( null ) ;
124+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
105125
106126 const result = await service . getUserLeaderboard ( ) ;
107127
@@ -111,91 +131,124 @@ describe('LeaderboardService', () => {
111131 it ( '쿼리 오류 발생 시 예외를 그대로 전파한다' , async ( ) => {
112132 const errorMessage = '사용자 리더보드 조회 중 문제가 발생했습니다.' ;
113133 const dbError = new DBError ( errorMessage ) ;
114- repo . getUserLeaderboard . mockRejectedValue ( dbError ) ;
134+
135+ mockCache . get . mockResolvedValue ( null ) ;
136+ mockRepo . getUserLeaderboard . mockRejectedValue ( dbError ) ;
115137
116138 await expect ( service . getUserLeaderboard ( ) ) . rejects . toThrow ( errorMessage ) ;
117- expect ( repo . getUserLeaderboard ) . toHaveBeenCalledTimes ( 1 ) ;
139+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledTimes ( 1 ) ;
140+ } ) ;
141+
142+ it ( '캐시 히트 시 Repository를 호출하지 않고 캐시된 데이터를 반환해야 한다' , async ( ) => {
143+ mockCache . get . mockResolvedValue ( mockResult ) ;
144+
145+ const result = await service . getUserLeaderboard ( 'viewCount' , 30 , 10 ) ;
146+
147+ expect ( mockCache . get ) . toHaveBeenCalledWith ( 'leaderboard:user:viewCount:30:10' ) ;
148+ expect ( mockRepo . getUserLeaderboard ) . not . toHaveBeenCalled ( ) ;
149+ expect ( result ) . toEqual ( mockResult ) ;
150+ } ) ;
151+
152+ it ( '캐시 미스 시 Repository를 호출하고 결과를 캐싱해야 한다' , async ( ) => {
153+ mockCache . get . mockResolvedValue ( null ) ;
154+ mockRepo . getUserLeaderboard . mockResolvedValue ( mockRawResult ) ;
155+
156+ const result = await service . getUserLeaderboard ( 'postCount' , 30 , 10 ) ;
157+
158+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'postCount' , 30 , 10 ) ;
159+ expect ( mockCache . set ) . toHaveBeenCalledWith ( 'leaderboard:user:postCount:30:10' , mockResult , LEADERBOARD_CACHE_TTL ) ;
160+ expect ( result ) . toEqual ( mockResult ) ;
118161 } ) ;
119162 } ) ;
120163
121164 describe ( 'getPostLeaderboard' , ( ) => {
122- it ( '응답 형식에 맞게 변환된 게시물 리더보드 데이터를 반환해야 한다' , async ( ) => {
123- const mockRawResult = [
165+ const mockRawResult = [
166+ {
167+ id : '1' ,
168+ title : 'test' ,
169+ slug : 'test-slug' ,
170+ username : 'test' ,
171+ total_views : '100' ,
172+ total_likes : '50' ,
173+ view_diff : '20' ,
174+ like_diff : '10' ,
175+ released_at : '2025-01-01' ,
176+ } ,
177+ {
178+ id : '2' ,
179+ title : 'test2' ,
180+ slug : 'test2-slug' ,
181+ username : 'test2' ,
182+ total_views : '200' ,
183+ total_likes : '100' ,
184+ view_diff : '10' ,
185+ like_diff : '5' ,
186+ released_at : '2025-01-02' ,
187+ } ,
188+ ] ;
189+
190+ const mockResult = {
191+ posts : [
124192 {
125193 id : '1' ,
126194 title : 'test' ,
127195 slug : 'test-slug' ,
128196 username : 'test' ,
129- total_views : ' 100' ,
130- total_likes : '50' ,
131- view_diff : '20' ,
132- like_diff : '10' ,
133- released_at : '2025-01-01' ,
197+ totalViews : 100 ,
198+ totalLikes : 50 ,
199+ viewDiff : 20 ,
200+ likeDiff : 10 ,
201+ releasedAt : '2025-01-01' ,
134202 } ,
135203 {
136204 id : '2' ,
137205 title : 'test2' ,
138206 slug : 'test2-slug' ,
139207 username : 'test2' ,
140- total_views : ' 200' ,
141- total_likes : ' 100' ,
142- view_diff : '10' ,
143- like_diff : '5' ,
144- released_at : '2025-01-02' ,
208+ totalViews : 200 ,
209+ totalLikes : 100 ,
210+ viewDiff : 10 ,
211+ likeDiff : 5 ,
212+ releasedAt : '2025-01-02' ,
145213 } ,
146- ] ;
147-
148- const mockResult = {
149- posts : [
150- {
151- id : '1' ,
152- title : 'test' ,
153- slug : 'test-slug' ,
154- username : 'test' ,
155- totalViews : 100 ,
156- totalLikes : 50 ,
157- viewDiff : 20 ,
158- likeDiff : 10 ,
159- releasedAt : '2025-01-01' ,
160- } ,
161- {
162- id : '2' ,
163- title : 'test2' ,
164- slug : 'test2-slug' ,
165- username : 'test2' ,
166- totalViews : 200 ,
167- totalLikes : 100 ,
168- viewDiff : 10 ,
169- likeDiff : 5 ,
170- releasedAt : '2025-01-02' ,
171- } ,
172- ] ,
173- } ;
174-
175- repo . getPostLeaderboard . mockResolvedValue ( mockRawResult ) ;
214+ ] ,
215+ } ;
216+
217+ beforeEach ( ( ) => {
218+ jest . clearAllMocks ( ) ;
219+ } ) ;
220+
221+ it ( '응답 형식에 맞게 변환된 게시물 리더보드 데이터를 반환해야 한다' , async ( ) => {
222+ mockCache . get . mockResolvedValue ( null ) ;
223+ mockRepo . getPostLeaderboard . mockResolvedValue ( mockRawResult ) ;
224+
176225 const result = await service . getPostLeaderboard ( 'viewCount' , 30 , 10 ) ;
177226
178227 expect ( result . posts ) . toEqual ( mockResult . posts ) ;
179228 } ) ;
180229
181230 it ( '쿼리 파라미터가 올바르게 적용되어야 한다' , async ( ) => {
182- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
231+ mockCache . get . mockResolvedValue ( null ) ;
232+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
183233
184234 await service . getPostLeaderboard ( 'likeCount' , 30 , 10 ) ;
185235
186- expect ( repo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'likeCount' , 30 , 10 ) ;
236+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'likeCount' , 30 , 10 ) ;
187237 } ) ;
188238
189239 it ( '쿼리 파라미터가 입력되지 않은 경우 기본값으로 처리되어야 한다' , async ( ) => {
190- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
240+ mockCache . get . mockResolvedValue ( null ) ;
241+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
191242
192243 await service . getPostLeaderboard ( ) ;
193244
194- expect ( repo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
245+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
195246 } ) ;
196247
197248 it ( '데이터가 없는 경우 빈 배열을 반환해야 한다' , async ( ) => {
198- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
249+ mockCache . get . mockResolvedValue ( null ) ;
250+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
251+
199252 const result = await service . getPostLeaderboard ( ) ;
200253
201254 expect ( result ) . toEqual ( { posts : [ ] } ) ;
@@ -204,10 +257,33 @@ describe('LeaderboardService', () => {
204257 it ( '쿼리 오류 발생 시 예외를 그대로 전파한다' , async ( ) => {
205258 const errorMessage = '게시물 리더보드 조회 중 문제가 발생했습니다.' ;
206259 const dbError = new DBError ( errorMessage ) ;
207- repo . getPostLeaderboard . mockRejectedValue ( dbError ) ;
260+
261+ mockCache . get . mockResolvedValue ( null ) ;
262+ mockRepo . getPostLeaderboard . mockRejectedValue ( dbError ) ;
208263
209264 await expect ( service . getPostLeaderboard ( ) ) . rejects . toThrow ( errorMessage ) ;
210- expect ( repo . getPostLeaderboard ) . toHaveBeenCalledTimes ( 1 ) ;
265+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledTimes ( 1 ) ;
266+ } ) ;
267+
268+ it ( '캐시 히트 시 Repository를 호출하지 않고 캐시된 데이터를 반환해야 한다' , async ( ) => {
269+ mockCache . get . mockResolvedValue ( mockResult ) ;
270+
271+ const result = await service . getPostLeaderboard ( 'viewCount' , 30 , 10 ) ;
272+
273+ expect ( mockCache . get ) . toHaveBeenCalledWith ( 'leaderboard:post:viewCount:30:10' ) ;
274+ expect ( mockRepo . getPostLeaderboard ) . not . toHaveBeenCalled ( ) ;
275+ expect ( result ) . toEqual ( mockResult ) ;
276+ } ) ;
277+
278+ it ( '캐시 미스 시 Repository를 호출하고 결과를 캐싱해야 한다' , async ( ) => {
279+ mockCache . get . mockResolvedValue ( null ) ;
280+ mockRepo . getPostLeaderboard . mockResolvedValue ( mockRawResult ) ;
281+
282+ const result = await service . getPostLeaderboard ( 'likeCount' , 30 , 10 ) ;
283+
284+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'likeCount' , 30 , 10 ) ;
285+ expect ( mockCache . set ) . toHaveBeenCalledWith ( 'leaderboard:post:likeCount:30:10' , mockResult , LEADERBOARD_CACHE_TTL ) ;
286+ expect ( result ) . toEqual ( mockResult ) ;
211287 } ) ;
212288 } ) ;
213289} ) ;
0 commit comments