Unit tests for services
🏠The everyday picture
Testing a calculator's addition button on its own, with known sums, before testing whether the whole calculator works.
📘What it really is
- Services receive their repository (Module 43), so tests give them the in-memory one:
import { describe, it, expect, beforeEach } from "vitest";
describe("enquiryService.close", () => {
let repo: InMemoryEnquiryRepo;
beforeEach(() => { repo = new InMemoryEnquiryRepo([sampleNewEnquiry]); });
it("closes a new enquiry with a note", async () => {
const result = await close(repo, "E101", "Called back", staffUser);
expect(result.status).toBe("closed");
expect(result.closedBy).toBe(staffUser.id);
});
it("refuses to close an already closed enquiry", async () => {
await close(repo, "E101", "First", staffUser);
await expect(close(repo, "E101", "Second", staffUser)).rejects.toThrow(ConflictError);
});
});
beforeEachgives every test fresh data, so tests don't affect each other.- Test schemas directly too: valid data passes, and each kind of bad data fails with the expected message.
- Dates and randomness make tests unpredictable. Pass in the current time or an id generator, or use Vitest's fake
timers (
vi.useFakeTimers()), so results are the same every run.
🖱️Try it
Write unit tests for every service function: normal cases, not found, conflicts, and each rule in your TESTING.md.
Write tests for your Zod schemas with valid and invalid examples. Aim for at least 15 tests.
✅Quick check
Why use beforeEach to set up fresh data?
Show the answer
So each test starts the same and can't be affected by changes another test made.
A test passes on Monday and fails on Saturday. What might be the cause?
Show the answer
The code depends on the current date (like "working days"); pass in the date or use fake timers.
