1
1
import { Pool } from 'pg' ;
2
2
import { DBError } from '@/exception' ;
3
3
import { 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' ;
5
6
6
7
jest . mock ( '@/repositories/leaderboard.repository' ) ;
8
+ jest . mock ( '@/configs/cache.config' , ( ) => ( {
9
+ cache : {
10
+ get : jest . fn ( ) ,
11
+ set : jest . fn ( ) ,
12
+ } ,
13
+ } ) ) ;
7
14
8
15
describe ( 'LeaderboardService' , ( ) => {
9
16
let service : LeaderboardService ;
10
- let repo : jest . Mocked < LeaderboardRepository > ;
17
+ let mockRepo : jest . Mocked < LeaderboardRepository > ;
11
18
let mockPool : jest . Mocked < Pool > ;
19
+ let mockCache : jest . Mocked < ICache > ;
12
20
13
21
beforeEach ( ( ) => {
14
22
const mockPoolObj = { } ;
15
23
mockPool = mockPoolObj as jest . Mocked < Pool > ;
16
24
17
25
const repoInstance = new LeaderboardRepository ( mockPool ) ;
18
- repo = repoInstance as jest . Mocked < LeaderboardRepository > ;
26
+ mockRepo = repoInstance as jest . Mocked < LeaderboardRepository > ;
19
27
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 ) ;
21
32
} ) ;
22
33
23
34
afterEach ( ( ) => {
24
35
jest . clearAllMocks ( ) ;
25
36
} ) ;
26
37
27
38
describe ( 'getUserLeaderboard' , ( ) => {
28
- it ( '응답 형식에 맞게 변환된 사용자 리더보드 데이터를 반환해야 한다' , async ( ) => {
29
- const mockRawResult = [
39
+ const mockRawResult = [
40
+ {
41
+ id : '1' ,
42
+
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
+
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 : [
30
66
{
31
67
id : '1' ,
32
68
33
69
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 ,
40
76
} ,
41
77
{
42
78
id : '2' ,
43
79
44
80
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 ,
51
87
} ,
52
- ] ;
53
-
54
- const mockResult = {
55
- users : [
56
- {
57
- id : '1' ,
58
-
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
-
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
+
82
99
const result = await service . getUserLeaderboard ( 'viewCount' , 30 , 10 ) ;
83
100
84
101
expect ( result . users ) . toEqual ( mockResult . users ) ;
85
102
} ) ;
86
103
87
104
it ( '쿼리 파라미터가 올바르게 적용되어야 한다' , async ( ) => {
88
- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
105
+ mockCache . get . mockResolvedValue ( null ) ;
106
+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
89
107
90
108
await service . getUserLeaderboard ( 'postCount' , 30 , 10 ) ;
91
109
92
- expect ( repo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'postCount' , 30 , 10 ) ;
110
+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'postCount' , 30 , 10 ) ;
93
111
} ) ;
94
112
95
113
it ( '쿼리 파라미터가 입력되지 않은 경우 기본값으로 처리되어야 한다' , async ( ) => {
96
- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
114
+ mockCache . get . mockResolvedValue ( null ) ;
115
+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
97
116
98
117
await service . getUserLeaderboard ( ) ;
99
118
100
- expect ( repo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
119
+ expect ( mockRepo . getUserLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
101
120
} ) ;
102
121
103
122
it ( '데이터가 없는 경우 빈 배열을 반환해야 한다' , async ( ) => {
104
- repo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
123
+ mockCache . get . mockResolvedValue ( null ) ;
124
+ mockRepo . getUserLeaderboard . mockResolvedValue ( [ ] ) ;
105
125
106
126
const result = await service . getUserLeaderboard ( ) ;
107
127
@@ -111,91 +131,124 @@ describe('LeaderboardService', () => {
111
131
it ( '쿼리 오류 발생 시 예외를 그대로 전파한다' , async ( ) => {
112
132
const errorMessage = '사용자 리더보드 조회 중 문제가 발생했습니다.' ;
113
133
const dbError = new DBError ( errorMessage ) ;
114
- repo . getUserLeaderboard . mockRejectedValue ( dbError ) ;
134
+
135
+ mockCache . get . mockResolvedValue ( null ) ;
136
+ mockRepo . getUserLeaderboard . mockRejectedValue ( dbError ) ;
115
137
116
138
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 ) ;
118
161
} ) ;
119
162
} ) ;
120
163
121
164
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 : [
124
192
{
125
193
id : '1' ,
126
194
title : 'test' ,
127
195
slug : 'test-slug' ,
128
196
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' ,
134
202
} ,
135
203
{
136
204
id : '2' ,
137
205
title : 'test2' ,
138
206
slug : 'test2-slug' ,
139
207
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' ,
145
213
} ,
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
+
176
225
const result = await service . getPostLeaderboard ( 'viewCount' , 30 , 10 ) ;
177
226
178
227
expect ( result . posts ) . toEqual ( mockResult . posts ) ;
179
228
} ) ;
180
229
181
230
it ( '쿼리 파라미터가 올바르게 적용되어야 한다' , async ( ) => {
182
- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
231
+ mockCache . get . mockResolvedValue ( null ) ;
232
+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
183
233
184
234
await service . getPostLeaderboard ( 'likeCount' , 30 , 10 ) ;
185
235
186
- expect ( repo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'likeCount' , 30 , 10 ) ;
236
+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'likeCount' , 30 , 10 ) ;
187
237
} ) ;
188
238
189
239
it ( '쿼리 파라미터가 입력되지 않은 경우 기본값으로 처리되어야 한다' , async ( ) => {
190
- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
240
+ mockCache . get . mockResolvedValue ( null ) ;
241
+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
191
242
192
243
await service . getPostLeaderboard ( ) ;
193
244
194
- expect ( repo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
245
+ expect ( mockRepo . getPostLeaderboard ) . toHaveBeenCalledWith ( 'viewCount' , 30 , 10 ) ;
195
246
} ) ;
196
247
197
248
it ( '데이터가 없는 경우 빈 배열을 반환해야 한다' , async ( ) => {
198
- repo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
249
+ mockCache . get . mockResolvedValue ( null ) ;
250
+ mockRepo . getPostLeaderboard . mockResolvedValue ( [ ] ) ;
251
+
199
252
const result = await service . getPostLeaderboard ( ) ;
200
253
201
254
expect ( result ) . toEqual ( { posts : [ ] } ) ;
@@ -204,10 +257,33 @@ describe('LeaderboardService', () => {
204
257
it ( '쿼리 오류 발생 시 예외를 그대로 전파한다' , async ( ) => {
205
258
const errorMessage = '게시물 리더보드 조회 중 문제가 발생했습니다.' ;
206
259
const dbError = new DBError ( errorMessage ) ;
207
- repo . getPostLeaderboard . mockRejectedValue ( dbError ) ;
260
+
261
+ mockCache . get . mockResolvedValue ( null ) ;
262
+ mockRepo . getPostLeaderboard . mockRejectedValue ( dbError ) ;
208
263
209
264
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 ) ;
211
287
} ) ;
212
288
} ) ;
213
289
} ) ;
0 commit comments