Skip to content

Commit 42472b9

Browse files
authored
[25.04.30 / TASK-177] Test - 사용자 / 게시물 Leaderboard 조회 API 통합 테스트 추가 (#28)
1 parent 20bcd49 commit 42472b9

File tree

5 files changed

+414
-83
lines changed

5 files changed

+414
-83
lines changed
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
import logger from '@/configs/logger.config';
2+
import dotenv from 'dotenv';
3+
import pg from 'pg';
4+
import { Pool } from 'pg';
5+
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
6+
import { PostLeaderboardSortType, UserLeaderboardSortType } from '@/types';
7+
8+
dotenv.config();
9+
10+
jest.setTimeout(20000); // 각 케이스당 20초 타임아웃 설정
11+
12+
/**
13+
* LeaderboardRepository 통합 테스트
14+
*
15+
* 이 테스트 파일은 실제 데이터베이스와 연결하여 LeaderboardRepository의 모든 메서드를
16+
* 실제 환경과 동일한 조건에서 테스트합니다.
17+
*/
18+
describe('LeaderboardRepository 통합 테스트', () => {
19+
let testPool: Pool;
20+
let repo: LeaderboardRepository;
21+
22+
// eslint-disable-next-line @typescript-eslint/naming-convention
23+
const DEFAULT_PARAMS = {
24+
USER_SORT: 'viewCount' as UserLeaderboardSortType,
25+
POST_SORT: 'viewCount' as PostLeaderboardSortType,
26+
DATE_RANGE: 30,
27+
LIMIT: 10,
28+
};
29+
30+
beforeAll(async () => {
31+
try {
32+
const testPoolConfig: pg.PoolConfig = {
33+
database: process.env.DATABASE_NAME,
34+
user: process.env.POSTGRES_USER,
35+
host: process.env.POSTGRES_HOST,
36+
password: process.env.POSTGRES_PASSWORD,
37+
port: Number(process.env.POSTGRES_PORT),
38+
max: 1, // 최대 연결 수
39+
idleTimeoutMillis: 30000, // 연결 유휴 시간 (30초)
40+
connectionTimeoutMillis: 5000, // 연결 시간 초과 (5초)
41+
allowExitOnIdle: false, // 유휴 상태에서 종료 허용
42+
statement_timeout: 30000,
43+
};
44+
45+
// localhost 가 아니면 ssl 필수
46+
if (process.env.POSTGRES_HOST != 'localhost') {
47+
testPoolConfig.ssl = {
48+
rejectUnauthorized: false,
49+
};
50+
}
51+
52+
testPool = new Pool(testPoolConfig);
53+
54+
// 연결 확인
55+
await testPool.query('SELECT 1');
56+
logger.info('LeaderboardRepository 통합 테스트 DB 연결 성공');
57+
58+
// 리포지토리 인스턴스 생성
59+
repo = new LeaderboardRepository(testPool);
60+
61+
// 충분한 데이터가 있는지 확인 (limit 기본값인 10을 기준으로 함)
62+
const userCheck = await testPool.query('SELECT COUNT(*) >= 10 AS is_enough FROM users_user');
63+
const postCheck = await testPool.query('SELECT COUNT(*) >= 10 AS is_enough FROM posts_post');
64+
const statsCheck = await testPool.query('SELECT COUNT(*) > 0 AS is_enough FROM posts_postdailystatistics');
65+
66+
if (!userCheck.rows[0].is_enough || !postCheck.rows[0].is_enough || !statsCheck.rows[0].is_enough) {
67+
logger.warn('주의: LeaderboardRepository 통합 테스트를 위한 충분한 데이터가 없습니다.');
68+
}
69+
} catch (error) {
70+
logger.error('LeaderboardRepository 통합 테스트 설정 중 오류 발생:', error);
71+
throw error;
72+
}
73+
});
74+
75+
afterAll(async () => {
76+
try {
77+
jest.clearAllMocks();
78+
79+
// 풀 완전 종료
80+
await testPool.end();
81+
82+
logger.info('LeaderboardRepository 통합 테스트 DB 연결 종료');
83+
} catch (error) {
84+
logger.error('LeaderboardRepository 통합 테스트 종료 중 오류:', error);
85+
}
86+
});
87+
88+
describe('getUserLeaderboard', () => {
89+
it('사용자 통계 배열로 이루어진 리더보드를 반환해야 한다', async () => {
90+
const result = await repo.getUserLeaderboard(
91+
DEFAULT_PARAMS.USER_SORT,
92+
DEFAULT_PARAMS.DATE_RANGE,
93+
DEFAULT_PARAMS.LIMIT,
94+
);
95+
96+
expect(Array.isArray(result)).toBe(true);
97+
98+
if (!isEnoughData(result, 1, '사용자 리더보드 반환값')) return;
99+
100+
result.forEach((leaderboardUser) => {
101+
expect(leaderboardUser).toHaveProperty('id');
102+
expect(leaderboardUser).toHaveProperty('email');
103+
expect(leaderboardUser).toHaveProperty('total_views');
104+
expect(leaderboardUser).toHaveProperty('total_likes');
105+
expect(leaderboardUser).toHaveProperty('total_posts');
106+
expect(leaderboardUser).toHaveProperty('view_diff');
107+
expect(leaderboardUser).toHaveProperty('like_diff');
108+
expect(leaderboardUser).toHaveProperty('post_diff');
109+
});
110+
});
111+
112+
it('통계와 관련된 필드는 음수가 아니어야 한다', async () => {
113+
const result = await repo.getUserLeaderboard(
114+
DEFAULT_PARAMS.USER_SORT,
115+
DEFAULT_PARAMS.DATE_RANGE,
116+
DEFAULT_PARAMS.LIMIT,
117+
);
118+
119+
if (!isEnoughData(result, 1, '사용자 리더보드 반환값')) return;
120+
121+
result.forEach((leaderboardUser) => {
122+
expect(Number(leaderboardUser.total_views)).toBeGreaterThanOrEqual(0);
123+
expect(Number(leaderboardUser.total_likes)).toBeGreaterThanOrEqual(0);
124+
expect(Number(leaderboardUser.total_posts)).toBeGreaterThanOrEqual(0);
125+
expect(Number(leaderboardUser.view_diff)).toBeGreaterThanOrEqual(0);
126+
expect(Number(leaderboardUser.like_diff)).toBeGreaterThanOrEqual(0);
127+
expect(Number(leaderboardUser.post_diff)).toBeGreaterThanOrEqual(0);
128+
});
129+
});
130+
131+
describe.each([
132+
{ sort: 'viewCount', field: 'view_diff' },
133+
{ sort: 'likeCount', field: 'like_diff' },
134+
{ sort: 'postCount', field: 'post_diff' },
135+
])('sort 파라미터에 따라 내림차순 정렬되어야 한다', ({ sort, field }) => {
136+
it(`sort가 ${sort}인 경우 ${field} 필드를 기준으로 정렬해야 한다`, async () => {
137+
const result = await repo.getUserLeaderboard(
138+
sort as UserLeaderboardSortType,
139+
DEFAULT_PARAMS.DATE_RANGE,
140+
DEFAULT_PARAMS.LIMIT,
141+
);
142+
143+
if (!isEnoughData(result, 2, `사용자 리더보드 정렬 (${sort})`)) return;
144+
145+
const isSorted = result.every((leaderboardUser, idx) => {
146+
if (idx === 0) return true;
147+
return Number(leaderboardUser[field]) <= Number(result[idx - 1][field]);
148+
});
149+
150+
expect(isSorted).toBe(true);
151+
});
152+
});
153+
154+
it('다양한 정렬 기준으로 결과를 반환해야 한다', async () => {
155+
const resultByViewDiff = await repo.getUserLeaderboard(
156+
'viewCount',
157+
DEFAULT_PARAMS.DATE_RANGE,
158+
DEFAULT_PARAMS.LIMIT,
159+
);
160+
const resultByLikeDiff = await repo.getUserLeaderboard(
161+
'likeCount',
162+
DEFAULT_PARAMS.DATE_RANGE,
163+
DEFAULT_PARAMS.LIMIT,
164+
);
165+
const resultByPostDiff = await repo.getUserLeaderboard(
166+
'postCount',
167+
DEFAULT_PARAMS.DATE_RANGE,
168+
DEFAULT_PARAMS.LIMIT,
169+
);
170+
171+
if (!isEnoughData(resultByViewDiff, 2, '사용자 리더보드 정렬')) return;
172+
173+
// 정렬 기준에 따라 결과가 달라야 하나, 순위가 같을 수 있어 하나라도 다르면 통과
174+
const areDifferent = resultByViewDiff.some(
175+
(userByViewDiff, idx) =>
176+
userByViewDiff.id !== resultByLikeDiff[idx].id || userByViewDiff.id !== resultByPostDiff[idx].id,
177+
);
178+
179+
// 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증
180+
if (areDifferent) {
181+
// eslint-disable-next-line jest/no-conditional-expect
182+
expect(areDifferent).toBe(true);
183+
}
184+
});
185+
186+
it('limit 파라미터가 결과 개수를 제한해야 한다', async () => {
187+
const limit5Result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, DEFAULT_PARAMS.DATE_RANGE, 5);
188+
const limit10Result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, DEFAULT_PARAMS.DATE_RANGE, 10);
189+
190+
if (!isEnoughData(limit10Result, 10, '사용자 리더보드 limit 파라미터')) return;
191+
192+
expect(limit5Result.length).toBe(5);
193+
expect(limit10Result.length).toBe(10);
194+
});
195+
196+
it('dateRange 파라미터를 통한 날짜 범위가 적용되어야 한다', async () => {
197+
const range3Result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, 3, DEFAULT_PARAMS.LIMIT);
198+
const range30Result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, 30, DEFAULT_PARAMS.LIMIT);
199+
200+
if (!isEnoughData(range3Result, 2, '사용자 리더보드 dateRange 파라미터')) return;
201+
202+
// 3일 범위 결과와 30일 범위 결과가 달라야 하나, 순위가 같을 수 있어 하나라도 다르면 통과
203+
const areDifferent = range3Result.some((userBy3Days, idx) => userBy3Days.id !== range30Result[idx].id);
204+
205+
// 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증
206+
if (areDifferent) {
207+
// eslint-disable-next-line jest/no-conditional-expect
208+
expect(areDifferent).toBe(true);
209+
}
210+
});
211+
212+
it('email이 null인 사용자는 제외되어야 한다', async () => {
213+
const result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, DEFAULT_PARAMS.DATE_RANGE, 30);
214+
215+
if (!isEnoughData(result, 1, '사용자 리더보드 email null 제외')) return;
216+
217+
result.forEach((user) => {
218+
expect(user.email).not.toBeNull();
219+
});
220+
});
221+
});
222+
223+
describe('getPostLeaderboard', () => {
224+
it('게시물 통계 배열로 이루어진 리더보드를 반환해야 한다', async () => {
225+
const result = await repo.getPostLeaderboard(
226+
DEFAULT_PARAMS.POST_SORT,
227+
DEFAULT_PARAMS.DATE_RANGE,
228+
DEFAULT_PARAMS.LIMIT,
229+
);
230+
231+
expect(Array.isArray(result)).toBe(true);
232+
233+
if (!isEnoughData(result, 1, '게시물 리더보드 반환값')) return;
234+
235+
result.forEach((leaderboardPost) => {
236+
expect(leaderboardPost).toHaveProperty('id');
237+
expect(leaderboardPost).toHaveProperty('title');
238+
expect(leaderboardPost).toHaveProperty('slug');
239+
expect(leaderboardPost).toHaveProperty('total_views');
240+
expect(leaderboardPost).toHaveProperty('total_likes');
241+
expect(leaderboardPost).toHaveProperty('view_diff');
242+
expect(leaderboardPost).toHaveProperty('like_diff');
243+
expect(leaderboardPost).toHaveProperty('released_at');
244+
});
245+
});
246+
247+
it('통계와 관련된 필드는 음수가 아니어야 한다', async () => {
248+
const result = await repo.getPostLeaderboard(
249+
DEFAULT_PARAMS.POST_SORT,
250+
DEFAULT_PARAMS.DATE_RANGE,
251+
DEFAULT_PARAMS.LIMIT,
252+
);
253+
254+
if (!isEnoughData(result, 1, '게시물 리더보드 반환값')) return;
255+
256+
result.forEach((leaderboardPost) => {
257+
expect(Number(leaderboardPost.total_views)).toBeGreaterThanOrEqual(0);
258+
expect(Number(leaderboardPost.total_likes)).toBeGreaterThanOrEqual(0);
259+
expect(Number(leaderboardPost.view_diff)).toBeGreaterThanOrEqual(0);
260+
expect(Number(leaderboardPost.like_diff)).toBeGreaterThanOrEqual(0);
261+
});
262+
});
263+
264+
describe.each([
265+
{ sort: 'viewCount', field: 'view_diff' },
266+
{ sort: 'likeCount', field: 'like_diff' },
267+
])('sort 파라미터에 따라 내림차순 정렬되어야 한다', ({ sort, field }) => {
268+
it(`sort가 ${sort}인 경우 ${field} 필드를 기준으로 정렬해야 한다`, async () => {
269+
const result = await repo.getPostLeaderboard(
270+
sort as PostLeaderboardSortType,
271+
DEFAULT_PARAMS.DATE_RANGE,
272+
DEFAULT_PARAMS.LIMIT,
273+
);
274+
275+
if (!isEnoughData(result, 2, `게시물 리더보드 정렬 (${sort})`)) return;
276+
277+
const isSorted = result.every((leaderboardPost, idx) => {
278+
if (idx === 0) return true;
279+
return Number(leaderboardPost[field]) <= Number(result[idx - 1][field]);
280+
});
281+
282+
expect(isSorted).toBe(true);
283+
});
284+
});
285+
286+
it('다양한 정렬 기준으로 결과를 반환해야 한다', async () => {
287+
const resultByViewDiff = await repo.getPostLeaderboard(
288+
'viewCount',
289+
DEFAULT_PARAMS.DATE_RANGE,
290+
DEFAULT_PARAMS.LIMIT,
291+
);
292+
const resultByLikeDiff = await repo.getPostLeaderboard(
293+
'likeCount',
294+
DEFAULT_PARAMS.DATE_RANGE,
295+
DEFAULT_PARAMS.LIMIT,
296+
);
297+
298+
if (!isEnoughData(resultByViewDiff, 2, '게시물 리더보드 정렬')) return;
299+
300+
// 정렬 기준에 따라 결과가 달라야 하나, 순위가 같을 수 있어 하나라도 다르면 통과
301+
const areDifferent = resultByViewDiff.some(
302+
(postByViewDiff, idx) => postByViewDiff.id !== resultByLikeDiff[idx].id,
303+
);
304+
305+
// 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증
306+
if (areDifferent) {
307+
// eslint-disable-next-line jest/no-conditional-expect
308+
expect(areDifferent).toBe(true);
309+
}
310+
});
311+
312+
it('limit 파라미터가 결과 개수를 제한해야 한다', async () => {
313+
const limit5Result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, DEFAULT_PARAMS.DATE_RANGE, 5);
314+
const limit10Result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, DEFAULT_PARAMS.DATE_RANGE, 10);
315+
316+
if (!isEnoughData(limit10Result, 10, '게시물 리더보드 limit 파라미터')) return;
317+
318+
expect(limit5Result.length).toBe(5);
319+
expect(limit10Result.length).toBe(10);
320+
});
321+
322+
it('dateRange 파라미터를 통한 날짜 범위가 적용되어야 한다', async () => {
323+
const range3Result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, 3, DEFAULT_PARAMS.LIMIT);
324+
const range30Result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, 30, DEFAULT_PARAMS.LIMIT);
325+
326+
if (!isEnoughData(range3Result, 2, '게시물 리더보드 dateRange 파라미터')) return;
327+
328+
// 3일 범위 결과와 30일 범위 결과가 달라야 하나, 순위가 같을 수 있어 하나라도 다르면 통과
329+
const areDifferent = range3Result.some((postBy3Days, idx) => postBy3Days.id !== range30Result[idx].id);
330+
331+
// 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증
332+
if (areDifferent) {
333+
// eslint-disable-next-line jest/no-conditional-expect
334+
expect(areDifferent).toBe(true);
335+
}
336+
});
337+
});
338+
});
339+
340+
function isEnoughData(result: unknown[], limit: number, testName: string): boolean {
341+
if (result.length < limit) {
342+
logger.info(`충분한 데이터가 없어 ${testName} 테스트를 건너뜁니다.`);
343+
return false;
344+
}
345+
return true;
346+
}

0 commit comments

Comments
 (0)