Skip to content

Commit 10db9aa

Browse files
authored
test: use an in-memory db instead of mocks (#49)
* ci: use run-p for parallel task execution * test: use an in-memory db instead of mocks
1 parent 672a82b commit 10db9aa

7 files changed

Lines changed: 210 additions & 325 deletions

File tree

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ export default {
161161
### Testing
162162

163163
- Write tests for services and utilities.
164-
- Use the existing mock utilities in `src/test/mocks/`.
164+
- Use `createTestDb()` from `src/test/create-test-db.ts` for service tests that need a real database.
165+
- Use the existing mock utilities in `src/test/mocks/` for Discord and API mocks.
165166
- Test both success and error cases.
166167
- Keep tests focused and independent.
167168

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"lint:fix": "oxlint src --fix",
3737
"test": "vitest run",
3838
"test:watch": "vitest",
39-
"verify": "run-p format:check lint tsc test",
39+
"verify": "run-p format lint:fix tsc test",
4040
"postinstall": "git config core.hooksPath .hooks && chmod +x .hooks/*"
4141
},
4242
"dependencies": {

src/services/poll.service.test.ts

Lines changed: 77 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
1-
import { beforeEach, describe, expect, it, vi } from "vitest";
1+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
22

3-
import { createMockPoll, createMockPollVote } from "../test/mocks/database.mock";
3+
import { cleanAllTables, createTestDb } from "../test/create-test-db";
44
import { PollService } from "./poll.service";
55

6-
const { mockDb } = vi.hoisted(() => {
7-
const mockDb: any = {
8-
select: vi.fn(() => mockDb),
9-
from: vi.fn(() => mockDb),
10-
where: vi.fn(() => Promise.resolve([])),
11-
insert: vi.fn(() => mockDb),
12-
values: vi.fn(() => mockDb),
13-
returning: vi.fn(() => Promise.resolve([])),
14-
};
15-
16-
return { mockDb };
17-
});
6+
let testDb: Awaited<ReturnType<typeof createTestDb>>;
187

19-
vi.mock("../database/db", () => ({ db: mockDb }));
8+
vi.mock("../database/db", () => ({
9+
get db() {
10+
return testDb;
11+
},
12+
}));
2013

2114
describe("Service: PollService", () => {
22-
beforeEach(() => {
23-
// ... reset all mocks before each test ...
24-
mockDb.select.mockClear().mockReturnValue(mockDb);
25-
mockDb.from.mockClear().mockReturnValue(mockDb);
26-
mockDb.where.mockClear().mockResolvedValue([]);
27-
mockDb.insert.mockClear().mockReturnValue(mockDb);
28-
mockDb.values.mockClear().mockReturnValue(mockDb);
29-
mockDb.returning.mockClear().mockResolvedValue([]);
15+
beforeAll(async () => {
16+
testDb = await createTestDb();
17+
});
18+
19+
beforeEach(async () => {
20+
await cleanAllTables(testDb);
3021
});
3122

3223
describe("createPoll", () => {
@@ -36,21 +27,6 @@ describe("Service: PollService", () => {
3627
});
3728

3829
it("creates a new poll with the provided details", async () => {
39-
// ARRANGE
40-
const mockPoll = createMockPoll({
41-
messageId: "msg123",
42-
channelId: "ch456",
43-
creatorId: "user789",
44-
question: "What's your favorite color?",
45-
options: JSON.stringify([
46-
{ text: "Red", votes: [] },
47-
{ text: "Blue", votes: [] },
48-
{ text: "Green", votes: [] },
49-
]),
50-
endTime: null,
51-
});
52-
mockDb.returning.mockResolvedValue([mockPoll]);
53-
5430
// ACT
5531
const result = await PollService.createPoll(
5632
"msg123",
@@ -61,27 +37,22 @@ describe("Service: PollService", () => {
6137
);
6238

6339
// ASSERT
64-
expect(mockDb.insert).toHaveBeenCalledWith(expect.anything());
65-
expect(mockDb.values).toHaveBeenCalledWith({
66-
messageId: "msg123",
67-
channelId: "ch456",
68-
creatorId: "user789",
69-
question: "What's your favorite color?",
70-
options: JSON.stringify([
71-
{ text: "Red", votes: [] },
72-
{ text: "Blue", votes: [] },
73-
{ text: "Green", votes: [] },
74-
]),
75-
endTime: undefined,
76-
});
77-
expect(result).toEqual(mockPoll);
40+
expect(result.messageId).toEqual("msg123");
41+
expect(result.channelId).toEqual("ch456");
42+
expect(result.creatorId).toEqual("user789");
43+
expect(result.question).toEqual("What's your favorite color?");
44+
expect(JSON.parse(result.options)).toEqual([
45+
{ text: "Red", votes: [] },
46+
{ text: "Blue", votes: [] },
47+
{ text: "Green", votes: [] },
48+
]);
49+
expect(result.endTime).toBeNull();
50+
expect(result.id).toBeDefined();
7851
});
7952

8053
it("creates a poll with an end time when provided", async () => {
8154
// ARRANGE
8255
const endTime = new Date("2024-12-31T23:59:59Z");
83-
const mockPoll = createMockPoll({ endTime });
84-
mockDb.returning.mockResolvedValue([mockPoll]);
8556

8657
// ACT
8758
const result = await PollService.createPoll(
@@ -94,11 +65,6 @@ describe("Service: PollService", () => {
9465
);
9566

9667
// ASSERT
97-
expect(mockDb.values).toHaveBeenCalledWith(
98-
expect.objectContaining({
99-
endTime,
100-
}),
101-
);
10268
expect(result.endTime).toEqual(endTime);
10369
});
10470
});
@@ -111,23 +77,17 @@ describe("Service: PollService", () => {
11177

11278
it("returns a poll when found", async () => {
11379
// ARRANGE
114-
const mockPoll = createMockPoll({ messageId: "msg123" });
115-
mockDb.where.mockResolvedValue([mockPoll]);
80+
await PollService.createPoll("msg123", "ch456", "user789", "Question?", ["A", "B"]);
11681

11782
// ACT
11883
const result = await PollService.getPoll("msg123");
11984

12085
// ASSERT
121-
expect(mockDb.select).toHaveBeenCalled();
122-
expect(mockDb.from).toHaveBeenCalledWith(expect.anything());
123-
expect(mockDb.where).toHaveBeenCalled();
124-
expect(result).toEqual(mockPoll);
86+
expect(result).not.toBeNull();
87+
expect(result?.messageId).toEqual("msg123");
12588
});
12689

12790
it("returns null when poll is not found", async () => {
128-
// ARRANGE
129-
mockDb.where.mockResolvedValue([]);
130-
13191
// ACT
13292
const result = await PollService.getPoll("nonexistent");
13393

@@ -144,50 +104,43 @@ describe("Service: PollService", () => {
144104

145105
it("adds a vote when user has not voted", async () => {
146106
// ARRANGE
147-
// ... mock getUserVote to return null ...
148-
mockDb.where.mockResolvedValueOnce([]);
107+
const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]);
149108

150109
// ACT
151-
const result = await PollService.addVote(1, "user123", 0);
110+
const result = await PollService.addVote(poll.id, "user123", 0);
152111

153112
// ASSERT
154-
expect(mockDb.insert).toHaveBeenCalledWith(expect.anything());
155-
expect(mockDb.values).toHaveBeenCalledWith({
156-
pollId: 1,
157-
userId: "user123",
158-
optionIndex: 0,
159-
});
160113
expect(result).toEqual(true);
114+
115+
const vote = await PollService.getUserVote(poll.id, "user123");
116+
expect(vote).not.toBeNull();
117+
expect(vote?.optionIndex).toEqual(0);
161118
});
162119

163120
it("returns false when user has already voted", async () => {
164121
// ARRANGE
165-
const existingVote = createMockPollVote();
166-
// ... mock getUserVote to return existing vote ...
167-
mockDb.where.mockResolvedValueOnce([existingVote]);
122+
const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]);
123+
await PollService.addVote(poll.id, "user123", 0);
168124

169125
// ACT
170-
const result = await PollService.addVote(1, "user123", 1);
126+
const result = await PollService.addVote(poll.id, "user123", 1);
171127

172128
// ASSERT
173-
expect(mockDb.insert).not.toHaveBeenCalled();
174129
expect(result).toEqual(false);
175130
});
176131

177132
it("allows voting for different option indices", async () => {
178133
// ARRANGE
179-
mockDb.where.mockResolvedValueOnce([]); // ... no existing vote ...
134+
const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B", "C"]);
180135

181136
// ACT
182-
const result = await PollService.addVote(1, "user123", 2);
137+
const result = await PollService.addVote(poll.id, "user123", 2);
183138

184139
// ASSERT
185-
expect(mockDb.values).toHaveBeenCalledWith({
186-
pollId: 1,
187-
userId: "user123",
188-
optionIndex: 2,
189-
});
190140
expect(result).toEqual(true);
141+
142+
const vote = await PollService.getUserVote(poll.id, "user123");
143+
expect(vote?.optionIndex).toEqual(2);
191144
});
192145
});
193146

@@ -199,29 +152,25 @@ describe("Service: PollService", () => {
199152

200153
it("returns a vote when user has voted", async () => {
201154
// ARRANGE
202-
const mockVote = createMockPollVote({
203-
pollId: 1,
204-
userId: "user123",
205-
optionIndex: 1,
206-
});
207-
mockDb.where.mockResolvedValue([mockVote]);
155+
const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]);
156+
await PollService.addVote(poll.id, "user123", 1);
208157

209158
// ACT
210-
const result = await PollService.getUserVote(1, "user123");
159+
const result = await PollService.getUserVote(poll.id, "user123");
211160

212161
// ASSERT
213-
expect(mockDb.select).toHaveBeenCalled();
214-
expect(mockDb.from).toHaveBeenCalledWith(expect.anything());
215-
expect(mockDb.where).toHaveBeenCalled();
216-
expect(result).toEqual(mockVote);
162+
expect(result).not.toBeNull();
163+
expect(result?.pollId).toEqual(poll.id);
164+
expect(result?.userId).toEqual("user123");
165+
expect(result?.optionIndex).toEqual(1);
217166
});
218167

219168
it("returns null when user has not voted", async () => {
220169
// ARRANGE
221-
mockDb.where.mockResolvedValue([]);
170+
const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]);
222171

223172
// ACT
224-
const result = await PollService.getUserVote(1, "user456");
173+
const result = await PollService.getUserVote(poll.id, "user456");
225174

226175
// ASSERT
227176
expect(result).toBeNull();
@@ -236,17 +185,15 @@ describe("Service: PollService", () => {
236185

237186
it("returns vote counts by option index", async () => {
238187
// ARRANGE
239-
const mockVotes = [
240-
createMockPollVote({ optionIndex: 0 }),
241-
createMockPollVote({ optionIndex: 0 }),
242-
createMockPollVote({ optionIndex: 1 }),
243-
createMockPollVote({ optionIndex: 0 }),
244-
createMockPollVote({ optionIndex: 2 }),
245-
];
246-
mockDb.where.mockResolvedValue(mockVotes);
188+
const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B", "C"]);
189+
await PollService.addVote(poll.id, "user1", 0);
190+
await PollService.addVote(poll.id, "user2", 0);
191+
await PollService.addVote(poll.id, "user3", 1);
192+
await PollService.addVote(poll.id, "user4", 0);
193+
await PollService.addVote(poll.id, "user5", 2);
247194

248195
// ACT
249-
const results = await PollService.getPollResults(1);
196+
const results = await PollService.getPollResults(poll.id);
250197

251198
// ASSERT
252199
expect(results).toBeInstanceOf(Map);
@@ -257,26 +204,31 @@ describe("Service: PollService", () => {
257204

258205
it("returns empty map when there are no votes", async () => {
259206
// ARRANGE
260-
mockDb.where.mockResolvedValue([]);
207+
const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]);
261208

262209
// ACT
263-
const results = await PollService.getPollResults(1);
210+
const results = await PollService.getPollResults(poll.id);
264211

265212
// ASSERT
266213
expect(results.size).toEqual(0);
267214
});
268215

269216
it("handles votes for non-sequential option indices", async () => {
270217
// ARRANGE
271-
const mockVotes = [
272-
createMockPollVote({ optionIndex: 0 }),
273-
createMockPollVote({ optionIndex: 5 }),
274-
createMockPollVote({ optionIndex: 5 }),
275-
];
276-
mockDb.where.mockResolvedValue(mockVotes);
218+
const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", [
219+
"A",
220+
"B",
221+
"C",
222+
"D",
223+
"E",
224+
"F",
225+
]);
226+
await PollService.addVote(poll.id, "user1", 0);
227+
await PollService.addVote(poll.id, "user2", 5);
228+
await PollService.addVote(poll.id, "user3", 5);
277229

278230
// ACT
279-
const results = await PollService.getPollResults(1);
231+
const results = await PollService.getPollResults(poll.id);
280232

281233
// ASSERT
282234
expect(results.get(0)).toEqual(1);
@@ -293,25 +245,20 @@ describe("Service: PollService", () => {
293245

294246
it("returns polls with no end time", async () => {
295247
// ARRANGE
296-
const mockPolls = [
297-
createMockPoll({ id: 1, endTime: null }),
298-
createMockPoll({ id: 2, endTime: null }),
299-
];
300-
mockDb.where.mockResolvedValue(mockPolls);
248+
await PollService.createPoll("msg1", "ch1", "creator1", "Q1?", ["A", "B"]);
249+
await PollService.createPoll("msg2", "ch1", "creator1", "Q2?", ["A", "B"]);
250+
await PollService.createPoll("msg3", "ch1", "creator1", "Q3?", ["A", "B"], new Date());
301251

302252
// ACT
303253
const result = await PollService.getActivePolls();
304254

305255
// ASSERT
306-
expect(mockDb.select).toHaveBeenCalled();
307-
expect(mockDb.from).toHaveBeenCalledWith(expect.anything());
308-
expect(mockDb.where).toHaveBeenCalled();
309-
expect(result).toEqual(mockPolls);
256+
expect(result).toHaveLength(2);
310257
});
311258

312259
it("returns empty array when there are no active polls", async () => {
313260
// ARRANGE
314-
mockDb.where.mockResolvedValue([]);
261+
await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"], new Date());
315262

316263
// ACT
317264
const result = await PollService.getActivePolls();

0 commit comments

Comments
 (0)