Skip to content

Commit 229396e

Browse files
committed
feature: repo 테스트에 필수인 모킹 객체들 fixture 로 분리, 공통 fixture 사용
1 parent 0334814 commit 229396e

File tree

7 files changed

+255
-34
lines changed

7 files changed

+255
-34
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { QueryResult } from "pg";
2+
3+
export const mockPool: {
4+
query: jest.Mock<Promise<QueryResult<Record<string, unknown>>>, unknown[]>;
5+
} = {
6+
query: jest.fn(),
7+
};
8+
9+
/**
10+
* pg의 QueryResult 타입을 만족하는 mock 객체를 생성하기 위한 헬퍼 함수
11+
* @param rows
12+
* @returns
13+
*/
14+
export function createMockQueryResult<T extends Record<string, unknown>>(rows: T[]): QueryResult<T> {
15+
return {
16+
rows,
17+
rowCount: rows.length,
18+
command: '',
19+
oid: 0,
20+
fields: [],
21+
} satisfies QueryResult<T>;
22+
}

src/repositories/__test__/leaderboard.repo.integration.test.ts renamed to src/repositories/__test__/integration/leaderboard.repo.integration.test.ts

File renamed without changes.

src/repositories/__test__/post.repo.integration.test.ts renamed to src/repositories/__test__/integration/post.repo.integration.test.ts

File renamed without changes.

src/repositories/__test__/qr.repo.integration.test.ts renamed to src/repositories/__test__/integration/qr.repo.integration.test.ts

File renamed without changes.

src/repositories/__test__/leaderboard.repo.test.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
1-
import { Pool, QueryResult } from 'pg';
1+
import { Pool } from 'pg';
22
import { DBError } from '@/exception';
33
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
44
import { UserLeaderboardSortType, PostLeaderboardSortType } from '@/types';
5+
import { mockPool, createMockQueryResult } from './fixture';
56

67
jest.mock('pg');
78

8-
// pg의 QueryResult 타입을 만족하는 mock 객체를 생성하기 위한 헬퍼 함수 생성
9-
function createMockQueryResult<T extends Record<string, unknown>>(rows: T[]): QueryResult<T> {
10-
return {
11-
rows,
12-
rowCount: rows.length,
13-
command: '',
14-
oid: 0,
15-
fields: [],
16-
} satisfies QueryResult<T>;
17-
}
18-
19-
const mockPool: {
20-
query: jest.Mock<Promise<QueryResult<Record<string, unknown>>>, unknown[]>;
21-
} = {
22-
query: jest.fn(),
23-
};
249

2510
describe('LeaderboardRepository', () => {
2611
let repo: LeaderboardRepository;

src/repositories/__test__/post.repo.test.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,10 @@
1-
import { Pool, QueryResult } from 'pg';
1+
import { Pool } from 'pg';
22
import { PostRepository } from '@/repositories/post.repository';
33
import { DBError } from '@/exception';
4+
import { mockPool, createMockQueryResult } from './fixture';
45

56
jest.mock('pg');
67

7-
// pg의 QueryResult 타입을 만족하는 mock 객체를 생성하기 위한 헬퍼 함수 생성
8-
function createMockQueryResult<T extends Record<string, unknown>>(rows: T[]): QueryResult<T> {
9-
return {
10-
rows,
11-
rowCount: rows.length,
12-
command: '',
13-
oid: 0,
14-
fields: [],
15-
} satisfies QueryResult<T>;
16-
}
17-
18-
const mockPool: {
19-
query: jest.Mock<Promise<QueryResult<Record<string, unknown>>>, unknown[]>;
20-
} = {
21-
query: jest.fn(),
22-
};
238

249
describe('PostRepository', () => {
2510
let repo: PostRepository;
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { Pool} from 'pg';
2+
import { TotalStatsRepository } from '@/repositories/totalStats.repository';
3+
import { DBError } from '@/exception';
4+
import { getKSTDateStringWithOffset } from '@/utils/date.util';
5+
import { createMockQueryResult } from './fixture';
6+
7+
// Mock dependencies
8+
jest.mock('@/configs/logger.config', () => ({
9+
error: jest.fn(),
10+
}));
11+
12+
jest.mock('@/utils/date.util', () => ({
13+
getKSTDateStringWithOffset: jest.fn(),
14+
}));
15+
16+
describe('TotalStatsRepository', () => {
17+
let repository: TotalStatsRepository;
18+
let mockPool: jest.Mocked<Pool>;
19+
let mockGetKSTDateStringWithOffset: jest.MockedFunction<typeof getKSTDateStringWithOffset>;
20+
21+
beforeEach(() => {
22+
mockPool = {
23+
query: jest.fn(),
24+
} as unknown as jest.Mocked<Pool>;
25+
26+
mockGetKSTDateStringWithOffset = getKSTDateStringWithOffset as jest.MockedFunction<typeof getKSTDateStringWithOffset>;
27+
28+
repository = new TotalStatsRepository(mockPool);
29+
jest.clearAllMocks();
30+
});
31+
32+
describe('getTotalStats', () => {
33+
const userId = 1;
34+
const period = 7;
35+
const mockStartDate = '2025-05-27';
36+
37+
beforeEach(() => {
38+
mockGetKSTDateStringWithOffset.mockReturnValue(mockStartDate);
39+
});
40+
41+
describe('view 타입 통계 조회', () => {
42+
it('조회수 통계를 성공적으로 조회해야 한다', async () => {
43+
// Given
44+
const mockViewStats = [
45+
{ date: '2025-05-27', total_value: '100' },
46+
{ date: '2025-05-28', total_value: '150' },
47+
{ date: '2025-05-29', total_value: '200' },
48+
];
49+
50+
mockPool.query.mockResolvedValue(createMockQueryResult(mockViewStats));
51+
52+
// When
53+
const result = await repository.getTotalStats(userId, period, 'view');
54+
55+
// Then
56+
expect(result).toEqual(mockViewStats);
57+
expect(mockGetKSTDateStringWithOffset).toHaveBeenCalledWith(-period * 24 * 60);
58+
expect(mockPool.query).toHaveBeenCalledWith(
59+
expect.stringContaining('SUM(pds.daily_view_count)'),
60+
[userId, mockStartDate]
61+
);
62+
});
63+
64+
it('조회수 통계 조회 시 DB 에러가 발생하면 DBError를 던져야 한다', async () => {
65+
// Given
66+
mockPool.query.mockRejectedValue(new Error('Database connection failed'));
67+
68+
// When & Then
69+
await expect(repository.getTotalStats(userId, period, 'view')).rejects.toThrow(
70+
new DBError('조회수 통계 조회 중 문제가 발생했습니다.')
71+
);
72+
});
73+
});
74+
75+
describe('like 타입 통계 조회', () => {
76+
it('좋아요 통계를 성공적으로 조회해야 한다', async () => {
77+
// Given
78+
const mockLikeStats = [
79+
{ date: '2025-05-27', total_value: '50' },
80+
{ date: '2025-05-28', total_value: '75' },
81+
{ date: '2025-05-29', total_value: '100' },
82+
];
83+
84+
mockPool.query.mockResolvedValue(createMockQueryResult(mockLikeStats));
85+
86+
// When
87+
const result = await repository.getTotalStats(userId, period, 'like');
88+
89+
// Then
90+
expect(result).toEqual(mockLikeStats);
91+
expect(mockGetKSTDateStringWithOffset).toHaveBeenCalledWith(-period * 24 * 60);
92+
expect(mockPool.query).toHaveBeenCalledWith(
93+
expect.stringContaining('SUM(pds.daily_like_count)'),
94+
[userId, mockStartDate]
95+
);
96+
});
97+
98+
it('좋아요 통계 조회 시 DB 에러가 발생하면 DBError를 던져야 한다', async () => {
99+
// Given
100+
mockPool.query.mockRejectedValue(new Error('Database connection failed'));
101+
102+
// When & Then
103+
await expect(repository.getTotalStats(userId, period, 'like')).rejects.toThrow(
104+
new DBError('좋아요 통계 조회 중 문제가 발생했습니다.')
105+
);
106+
});
107+
});
108+
109+
describe('post 타입 통계 조회', () => {
110+
it('게시글 통계를 성공적으로 조회해야 한다', async () => {
111+
// Given
112+
const mockPostStats = [
113+
{ date: '2025-05-27', total_value: 5 },
114+
{ date: '2025-05-28', total_value: 7 },
115+
{ date: '2025-05-29', total_value: 10 },
116+
];
117+
118+
mockPool.query.mockResolvedValue(createMockQueryResult(mockPostStats));
119+
120+
// When
121+
const result = await repository.getTotalStats(userId, period, 'post');
122+
123+
// Then
124+
expect(result).toEqual(mockPostStats);
125+
expect(mockGetKSTDateStringWithOffset).toHaveBeenCalledWith(-period * 24 * 60);
126+
expect(mockPool.query).toHaveBeenCalledWith(
127+
expect.stringContaining('WITH date_series AS'),
128+
[userId, mockStartDate]
129+
);
130+
expect(mockPool.query).toHaveBeenCalledWith(
131+
expect.stringContaining('SUM(dp.post_count) OVER'),
132+
[userId, mockStartDate]
133+
);
134+
});
135+
136+
it('게시글 통계 조회 시 DB 에러가 발생하면 DBError를 던져야 한다', async () => {
137+
// Given
138+
mockPool.query.mockRejectedValue(new Error('Database connection failed'));
139+
140+
// When & Then
141+
await expect(repository.getTotalStats(userId, period, 'post')).rejects.toThrow(
142+
new DBError('게시글 통계 조회 중 문제가 발생했습니다.')
143+
);
144+
});
145+
});
146+
147+
describe('잘못된 타입 처리', () => {
148+
it('지원되지 않는 통계 타입이 전달되면 DBError를 던져야 한다', async () => {
149+
// When & Then
150+
await expect(
151+
repository.getTotalStats(userId, period, 'invalid' as any)
152+
).rejects.toThrow(new DBError('지원되지 않는 통계 타입입니다.'));
153+
154+
expect(mockPool.query).not.toHaveBeenCalled();
155+
});
156+
});
157+
158+
describe('다양한 기간 테스트', () => {
159+
it('30일 기간으로 통계를 조회할 수 있어야 한다', async () => {
160+
// Given
161+
const period30 = 30;
162+
const mockStats = [{ date: '2025-04-27', total_value: '1000' }];
163+
164+
mockPool.query.mockResolvedValue(createMockQueryResult(mockStats));
165+
166+
// When
167+
await repository.getTotalStats(userId, period30, 'view');
168+
169+
// Then
170+
expect(mockGetKSTDateStringWithOffset).toHaveBeenCalledWith(-period30 * 24 * 60);
171+
});
172+
});
173+
174+
describe('빈 결과 처리', () => {
175+
it('데이터가 없을 때 빈 배열을 반환해야 한다', async () => {
176+
// Given
177+
mockPool.query.mockResolvedValue(createMockQueryResult([]));
178+
179+
// When
180+
const result = await repository.getTotalStats(userId, period, 'view');
181+
182+
// Then
183+
expect(result).toEqual([]);
184+
expect(result).toHaveLength(0);
185+
});
186+
});
187+
188+
describe('SQL 쿼리 검증', () => {
189+
beforeEach(() => {
190+
mockPool.query.mockResolvedValue(createMockQueryResult([]));
191+
});
192+
193+
it('view 통계 쿼리가 올바른 테이블과 조건을 포함해야 한다', async () => {
194+
// When
195+
await repository.getTotalStats(userId, period, 'view');
196+
197+
// Then
198+
const calledQuery = mockPool.query.mock.calls[0][0] as string;
199+
expect(calledQuery).toContain('posts_postdailystatistics pds');
200+
expect(calledQuery).toContain('JOIN posts_post p ON p.id = pds.post_id');
201+
expect(calledQuery).toContain('p.user_id = $1');
202+
expect(calledQuery).toContain('p.is_active = true');
203+
expect(calledQuery).toContain('pds.date >= $2');
204+
expect(calledQuery).toContain('SUM(pds.daily_view_count)');
205+
});
206+
207+
it('like 통계 쿼리가 올바른 컬럼을 조회해야 한다', async () => {
208+
// When
209+
await repository.getTotalStats(userId, period, 'like');
210+
211+
// Then
212+
const calledQuery = mockPool.query.mock.calls[0][0] as string;
213+
expect(calledQuery).toContain('SUM(pds.daily_like_count)');
214+
});
215+
216+
it('post 통계 쿼리가 CTE와 윈도우 함수를 사용해야 한다', async () => {
217+
// When
218+
await repository.getTotalStats(userId, period, 'post');
219+
220+
// Then
221+
const calledQuery = mockPool.query.mock.calls[0][0] as string;
222+
expect(calledQuery).toContain('WITH date_series AS');
223+
expect(calledQuery).toContain('generate_series');
224+
expect(calledQuery).toContain('SUM(dp.post_count) OVER');
225+
expect(calledQuery).toContain('DATE(p.released_at)');
226+
});
227+
});
228+
});
229+
});

0 commit comments

Comments
 (0)