Skip to content

Commit 9a194dd

Browse files
committed
test: Leaderboard 조회 API 서비스, 리포지토리 계층 단위 테스트 추가
1 parent 3b26d44 commit 9a194dd

File tree

2 files changed

+431
-0
lines changed

2 files changed

+431
-0
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import { DBError } from '@/exception';
2+
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
3+
import { Pool, QueryResult } from 'pg';
4+
5+
jest.mock('pg');
6+
7+
const mockPool: {
8+
query: jest.Mock<Promise<QueryResult<Record<string, unknown>>>, unknown[]>;
9+
} = {
10+
query: jest.fn(),
11+
};
12+
13+
describe('LeaderboardRepository', () => {
14+
let repo: LeaderboardRepository;
15+
16+
beforeEach(() => {
17+
repo = new LeaderboardRepository(mockPool as unknown as Pool);
18+
});
19+
20+
describe('getLeaderboard', () => {
21+
it('type이 post인 경우 post 데이터를 반환해야 한다.', async () => {
22+
const mockResult = [
23+
{
24+
id: 2,
25+
title: 'test2',
26+
slug: 'test2',
27+
total_views: 200,
28+
total_likes: 100,
29+
view_diff: 20,
30+
like_diff: 10,
31+
released_at: '2025-01-02',
32+
},
33+
{
34+
id: 1,
35+
title: 'test',
36+
slug: 'test',
37+
total_views: 100,
38+
total_likes: 50,
39+
view_diff: 10,
40+
like_diff: 5,
41+
released_at: '2025-01-01',
42+
},
43+
];
44+
45+
mockPool.query.mockResolvedValue({
46+
rows: mockResult,
47+
rowCount: mockResult.length,
48+
} as unknown as QueryResult);
49+
50+
const result = await repo.getLeaderboard('post', 'viewCount', 30, 10);
51+
52+
expect(result).toEqual(mockResult);
53+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('FROM posts_post p'), expect.anything());
54+
});
55+
56+
it('type이 user인 경우 user 데이터를 반환해야 한다.', async () => {
57+
const mockResult = [
58+
{
59+
id: 1,
60+
email: 'test@test.com',
61+
total_views: 100,
62+
total_likes: 50,
63+
total_posts: 1,
64+
view_diff: 20,
65+
like_diff: 10,
66+
post_diff: 1,
67+
},
68+
{
69+
id: 2,
70+
email: 'test2@test.com',
71+
total_views: 200,
72+
total_likes: 100,
73+
total_posts: 2,
74+
view_diff: 10,
75+
like_diff: 5,
76+
post_diff: 1,
77+
},
78+
];
79+
80+
mockPool.query.mockResolvedValue({
81+
rows: mockResult,
82+
rowCount: mockResult.length,
83+
} as unknown as QueryResult);
84+
85+
const result = await repo.getLeaderboard('user', 'viewCount', 30, 10);
86+
87+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('FROM users_user u'), expect.anything());
88+
expect(result).toEqual(mockResult);
89+
});
90+
91+
it('sort가 조회수인 경우 정렬 순서를 보장해야 한다.', async () => {
92+
const mockResult = [
93+
{ view_diff: 20, like_diff: 5, post_diff: 1 },
94+
{ view_diff: 10, like_diff: 10, post_diff: 2 },
95+
];
96+
97+
mockPool.query.mockResolvedValue({
98+
rows: mockResult,
99+
rowCount: mockResult.length,
100+
} as unknown as QueryResult);
101+
102+
const result = await repo.getLeaderboard('user', 'viewCount', 30, 10);
103+
104+
expect(result).toEqual(mockResult);
105+
expect(result[0].view_diff).toBeGreaterThan(result[1].view_diff);
106+
});
107+
108+
it('sort가 좋아요 수인 경우 정렬 순서를 보장해야 한다.', async () => {
109+
const mockResult = [
110+
{ view_diff: 10, like_diff: 10, post_diff: 1 },
111+
{ view_diff: 20, like_diff: 5, post_diff: 1 },
112+
];
113+
114+
mockPool.query.mockResolvedValue({
115+
rows: mockResult,
116+
rowCount: mockResult.length,
117+
} as unknown as QueryResult);
118+
119+
const result = await repo.getLeaderboard('user', 'likeCount', 30, 10);
120+
121+
expect(result).toEqual(mockResult);
122+
expect(result[0].like_diff).toBeGreaterThan(result[1].like_diff);
123+
});
124+
125+
it('sort가 게시물 수인 경우 정렬 순서를 보장해야 한다.', async () => {
126+
const mockResult = [
127+
{ view_diff: 10, like_diff: 10, post_diff: 4 },
128+
{ view_diff: 20, like_diff: 5, post_diff: 1 },
129+
];
130+
131+
mockPool.query.mockResolvedValue({
132+
rows: mockResult,
133+
rowCount: mockResult.length,
134+
} as unknown as QueryResult);
135+
136+
const result = await repo.getLeaderboard('user', 'postCount', 30, 10);
137+
138+
expect(result).toEqual(mockResult);
139+
expect(result[0].post_diff).toBeGreaterThan(result[1].post_diff);
140+
});
141+
142+
it('limit 만큼의 데이터만 반환해야 한다', async () => {
143+
const mockData = [
144+
{ id: 1, title: 'test' },
145+
{ id: 2, title: 'test2' },
146+
{ id: 3, title: 'test3' },
147+
{ id: 4, title: 'test4' },
148+
{ id: 5, title: 'test5' },
149+
];
150+
const mockLimit = 5;
151+
152+
mockPool.query.mockResolvedValue({
153+
rows: mockData,
154+
rowCount: mockData.length,
155+
} as unknown as QueryResult);
156+
157+
const result = await repo.getLeaderboard('post', 'viewCount', 30, mockLimit);
158+
159+
expect(result).toEqual(mockData);
160+
expect(result.length).toEqual(mockLimit);
161+
162+
expect(mockPool.query).toHaveBeenCalledWith(
163+
expect.stringContaining('LIMIT $2'),
164+
expect.arrayContaining([30, mockLimit]),
165+
);
166+
});
167+
168+
it('type이 post이고 sort가 게시물 수인 경우 조회수를 기준으로 정렬해야 한다.', async () => {
169+
const mockResult = [
170+
{ total_views: 200, total_likes: 5, view_diff: 20, like_diff: 0 },
171+
{ total_views: 100, total_likes: 50, view_diff: 10, like_diff: 5 },
172+
];
173+
174+
mockPool.query.mockResolvedValue({
175+
rows: mockResult,
176+
rowCount: mockResult.length,
177+
} as unknown as QueryResult);
178+
179+
const result = await repo.getLeaderboard('post', 'postCount', 30, 10);
180+
181+
expect(result).toEqual(mockResult);
182+
expect(mockPool.query).toHaveBeenCalledWith(
183+
expect.stringContaining('ORDER BY view_diff DESC'),
184+
expect.anything(),
185+
);
186+
expect(result[0].view_diff).toBeGreaterThan(result[1].view_diff);
187+
});
188+
189+
it('user 타입에는 GROUP BY 절이 포함되어야 한다', async () => {
190+
mockPool.query.mockResolvedValue({
191+
rows: [],
192+
rowCount: 0,
193+
} as unknown as QueryResult);
194+
195+
await repo.getLeaderboard('user', 'viewCount', 30, 10);
196+
197+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('GROUP BY u.id'), expect.anything());
198+
});
199+
200+
it('post 타입에는 GROUP BY 절이 포함되지 않아야 한다', async () => {
201+
mockPool.query.mockResolvedValue({
202+
rows: [],
203+
rowCount: 0,
204+
} as unknown as QueryResult);
205+
206+
await repo.getLeaderboard('post', 'viewCount', 30, 10);
207+
208+
expect(mockPool.query).toHaveBeenCalledWith(expect.not.stringContaining('GROUP BY'), expect.anything());
209+
});
210+
211+
it('dateRange 파라미터가 쿼리에 올바르게 적용되어야 한다', async () => {
212+
const mockResult = [{ id: 1 }];
213+
const testDateRange = 30;
214+
215+
mockPool.query.mockResolvedValue({
216+
rows: mockResult,
217+
rowCount: mockResult.length,
218+
} as unknown as QueryResult);
219+
220+
await repo.getLeaderboard('user', 'viewCount', testDateRange, 10);
221+
222+
expect(mockPool.query).toHaveBeenCalledWith(
223+
expect.stringContaining('$1::int'),
224+
expect.arrayContaining([testDateRange, expect.anything()]),
225+
);
226+
});
227+
228+
it('유효하지 않은 sort 값이 전달되면 기본값(view_diff)을 사용해야 한다', async () => {
229+
const mockResult = [{ view_diff: 10 }];
230+
231+
mockPool.query.mockResolvedValue({
232+
rows: mockResult,
233+
rowCount: mockResult.length,
234+
} as unknown as QueryResult);
235+
236+
await repo.getLeaderboard('user', 'invalidSort', 30, 10);
237+
238+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('view_diff DESC'), expect.anything());
239+
});
240+
241+
it('유효하지 않은 type 값이 전달되면 기본값(user)을 사용해야 한다', async () => {
242+
const mockResult = [{ view_diff: 10 }];
243+
244+
mockPool.query.mockResolvedValue({
245+
rows: mockResult,
246+
rowCount: mockResult.length,
247+
} as unknown as QueryResult);
248+
249+
const result = await repo.getLeaderboard('invalidType', 'viewCount', 30, 10);
250+
251+
expect(result).toEqual(mockResult);
252+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('FROM users_user u'), expect.anything());
253+
});
254+
255+
it('데이터가 없는 경우 빈 배열을 반환해야 한다', async () => {
256+
mockPool.query.mockResolvedValue({
257+
rows: [],
258+
rowCount: 0,
259+
} as unknown as QueryResult);
260+
261+
const result = await repo.getLeaderboard('user', 'viewCount', 30, 10);
262+
263+
expect(result).toEqual([]);
264+
});
265+
266+
it('에러 발생 시 DBError를 던져야 한다', async () => {
267+
mockPool.query.mockRejectedValue(new Error('DB connection failed'));
268+
await expect(repo.getLeaderboard('post', 'postCount', 30, 10)).rejects.toThrow(DBError);
269+
});
270+
});
271+
});

0 commit comments

Comments
 (0)