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+ it ( '응답 형식에 맞게 변환된 사용자 리더보드 데이터를 반환해야 한다' , async ( ) => {
92+ mockCache . get . mockResolvedValue ( null ) ;
93+ mockRepo . getUserLeaderboard . mockResolvedValue ( mockRawResult ) ;
94+
8295 const result = await service . getUserLeaderboard ( 'viewCount' , 30 , 10 ) ;
8396
8497 expect ( result . users ) . toEqual ( mockResult . users ) ;
8598 } ) ;
8699
87100 it ( '쿼리 파라미터가 올바르게 적용되어야 한다' , async ( ) => {
88- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
101+ mockCache . get . mockResolvedValue ( null ) ;
102+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
89103
90104 await service . getUserLeaderboard ( 'postCount' , 30 , 10 ) ;
91105
92- expect ( repo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'postCount' , 30 , 10 ) ;
106+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'postCount' , 30 , 10 ) ;
93107 } ) ;
94108
95109 it ( '쿼리 파라미터가 입력되지 않은 경우 기본값으로 처리되어야 한다' , async ( ) => {
96- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
110+ mockCache . get . mockResolvedValue ( null ) ;
111+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
97112
98113 await service . getUserLeaderboard ( ) ;
99114
100- expect ( repo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
115+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
101116 } ) ;
102117
103118 it ( '데이터가 없는 경우 빈 배열을 반환해야 한다' , async ( ) => {
104- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
119+ mockCache . get . mockResolvedValue ( null ) ;
120+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
105121
106122 const result = await service . getUserLeaderboard ( ) ;
107123
@@ -111,91 +127,120 @@ describe('LeaderboardService', () => {
111127 it ( '쿼리 오류 발생 시 예외를 그대로 전파한다' , async ( ) => {
112128 const errorMessage = '사용자 리더보드 조회 중 문제가 발생했습니다.' ;
113129 const dbError = new DBError ( errorMessage ) ;
114- repo . getUserLeaderboard . mockRejectedValue ( dbError ) ;
130+
131+ mockCache . get . mockResolvedValue ( null ) ;
132+ mockRepo . getUserLeaderboard . mockRejectedValue ( dbError ) ;
115133
116134 await expect ( service . getUserLeaderboard ( ) ) . rejects . toThrow ( errorMessage ) ;
117- expect ( repo . getUserLeaderboard ) . toHaveBeenCalledTimes ( 1 ) ;
135+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledTimes ( 1 ) ;
136+ } ) ;
137+
138+ it ( '캐시 히트 시 Repository를 호출하지 않고 캐시된 데이터를 반환해야 한다' , async ( ) => {
139+ mockCache . get . mockResolvedValue ( mockResult ) ;
140+
141+ const result = await service . getUserLeaderboard ( 'viewCount' , 30 , 10 ) ;
142+
143+ expect ( mockCache . get ) . toHaveBeenCalledWith ( 'leaderboard:user:viewCount:30:10' ) ;
144+ expect ( mockRepo . getUserLeaderboard ) . not . toHaveBeenCalled ( ) ;
145+ expect ( result ) . toEqual ( mockResult ) ;
146+ } ) ;
147+
148+ it ( '캐시 미스 시 Repository를 호출하고 결과를 캐싱해야 한다' , async ( ) => {
149+ mockCache . get . mockResolvedValue ( null ) ;
150+ mockRepo . getUserLeaderboard . mockResolvedValue ( mockRawResult ) ;
151+
152+ const result = await service . getUserLeaderboard ( 'postCount' , 30 , 10 ) ;
153+
154+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'postCount' , 30 , 10 ) ;
155+ expect ( mockCache . set ) . toHaveBeenCalledWith ( 'leaderboard:user:postCount:30:10' , mockResult , LEADERBOARD_CACHE_TTL ) ;
156+ expect ( result ) . toEqual ( mockResult ) ;
118157 } ) ;
119158 } ) ;
120159
121160 describe ( 'getPostLeaderboard' , ( ) => {
122- it ( '응답 형식에 맞게 변환된 게시물 리더보드 데이터를 반환해야 한다' , async ( ) => {
123- const mockRawResult = [
161+ const mockRawResult = [
162+ {
163+ id : '1' ,
164+ title : 'test' ,
165+ slug : 'test-slug' ,
166+ username : 'test' ,
167+ total_views : '100' ,
168+ total_likes : '50' ,
169+ view_diff : '20' ,
170+ like_diff : '10' ,
171+ released_at : '2025-01-01' ,
172+ } ,
173+ {
174+ id : '2' ,
175+ title : 'test2' ,
176+ slug : 'test2-slug' ,
177+ username : 'test2' ,
178+ total_views : '200' ,
179+ total_likes : '100' ,
180+ view_diff : '10' ,
181+ like_diff : '5' ,
182+ released_at : '2025-01-02' ,
183+ } ,
184+ ] ;
185+
186+ const mockResult = {
187+ posts : [
124188 {
125189 id : '1' ,
126190 title : 'test' ,
127191 slug : 'test-slug' ,
128192 username : 'test' ,
129- total_views : ' 100' ,
130- total_likes : '50' ,
131- view_diff : '20' ,
132- like_diff : '10' ,
133- released_at : '2025-01-01' ,
193+ totalViews : 100 ,
194+ totalLikes : 50 ,
195+ viewDiff : 20 ,
196+ likeDiff : 10 ,
197+ releasedAt : '2025-01-01' ,
134198 } ,
135199 {
136200 id : '2' ,
137201 title : 'test2' ,
138202 slug : 'test2-slug' ,
139203 username : 'test2' ,
140- total_views : ' 200' ,
141- total_likes : ' 100' ,
142- view_diff : '10' ,
143- like_diff : '5' ,
144- released_at : '2025-01-02' ,
204+ totalViews : 200 ,
205+ totalLikes : 100 ,
206+ viewDiff : 10 ,
207+ likeDiff : 5 ,
208+ releasedAt : '2025-01-02' ,
145209 } ,
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 ) ;
210+ ] ,
211+ } ;
212+
213+ it ( '응답 형식에 맞게 변환된 게시물 리더보드 데이터를 반환해야 한다' , async ( ) => {
214+ mockCache . get . mockResolvedValue ( null ) ;
215+ mockRepo . getPostLeaderboard . mockResolvedValue ( mockRawResult ) ;
216+
176217 const result = await service . getPostLeaderboard ( 'viewCount' , 30 , 10 ) ;
177218
178219 expect ( result . posts ) . toEqual ( mockResult . posts ) ;
179220 } ) ;
180221
181222 it ( '쿼리 파라미터가 올바르게 적용되어야 한다' , async ( ) => {
182- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
223+ mockCache . get . mockResolvedValue ( null ) ;
224+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
183225
184226 await service . getPostLeaderboard ( 'likeCount' , 30 , 10 ) ;
185227
186- expect ( repo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'likeCount' , 30 , 10 ) ;
228+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'likeCount' , 30 , 10 ) ;
187229 } ) ;
188230
189231 it ( '쿼리 파라미터가 입력되지 않은 경우 기본값으로 처리되어야 한다' , async ( ) => {
190- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
232+ mockCache . get . mockResolvedValue ( null ) ;
233+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
191234
192235 await service . getPostLeaderboard ( ) ;
193236
194- expect ( repo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
237+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
195238 } ) ;
196239
197240 it ( '데이터가 없는 경우 빈 배열을 반환해야 한다' , async ( ) => {
198- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
241+ mockCache . get . mockResolvedValue ( null ) ;
242+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
243+
199244 const result = await service . getPostLeaderboard ( ) ;
200245
201246 expect ( result ) . toEqual ( { posts : [ ] } ) ;
@@ -204,10 +249,33 @@ describe('LeaderboardService', () => {
204249 it ( '쿼리 오류 발생 시 예외를 그대로 전파한다' , async ( ) => {
205250 const errorMessage = '게시물 리더보드 조회 중 문제가 발생했습니다.' ;
206251 const dbError = new DBError ( errorMessage ) ;
207- repo . getPostLeaderboard . mockRejectedValue ( dbError ) ;
252+
253+ mockCache . get . mockResolvedValue ( null ) ;
254+ mockRepo . getPostLeaderboard . mockRejectedValue ( dbError ) ;
208255
209256 await expect ( service . getPostLeaderboard ( ) ) . rejects . toThrow ( errorMessage ) ;
210- expect ( repo . getPostLeaderboard ) . toHaveBeenCalledTimes ( 1 ) ;
257+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledTimes ( 1 ) ;
258+ } ) ;
259+
260+ it ( '캐시 히트 시 Repository를 호출하지 않고 캐시된 데이터를 반환해야 한다' , async ( ) => {
261+ mockCache . get . mockResolvedValue ( mockResult ) ;
262+
263+ const result = await service . getPostLeaderboard ( 'viewCount' , 30 , 10 ) ;
264+
265+ expect ( mockCache . get ) . toHaveBeenCalledWith ( 'leaderboard:post:viewCount:30:10' ) ;
266+ expect ( mockRepo . getPostLeaderboard ) . not . toHaveBeenCalled ( ) ;
267+ expect ( result ) . toEqual ( mockResult ) ;
268+ } ) ;
269+
270+ it ( '캐시 미스 시 Repository를 호출하고 결과를 캐싱해야 한다' , async ( ) => {
271+ mockCache . get . mockResolvedValue ( null ) ;
272+ mockRepo . getPostLeaderboard . mockResolvedValue ( mockRawResult ) ;
273+
274+ const result = await service . getPostLeaderboard ( 'likeCount' , 30 , 10 ) ;
275+
276+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'likeCount' , 30 , 10 ) ;
277+ expect ( mockCache . set ) . toHaveBeenCalledWith ( 'leaderboard:post:likeCount:30:10' , mockResult , LEADERBOARD_CACHE_TTL ) ;
278+ expect ( result ) . toEqual ( mockResult ) ;
211279 } ) ;
212280 } ) ;
213281} ) ;
0 commit comments