Skip to content

Commit 1202fd2

Browse files
committed
test: Leaderboard 리포지토리 계층 통합 테스트 추가
1 parent 07eaadc commit 1202fd2

File tree

1 file changed

+348
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)