Time and random IDs look harmless until a test needs an exact result. Calling new Date() and crypto.randomUUID() inside business logic introduces two invisible inputs.
export type Clock = () => Date;
export type IdGenerator = () => string;
type Dependencies = {
clock: Clock;
generateId: IdGenerator;
reminders: ReminderRepository;
};
The use case receives them like any other dependency:
export const scheduleReminder =
(dependencies: Dependencies) => async (userId: string, message: string) => {
const createdAt = dependencies.clock();
const reminder = {
id: dependencies.generateId(),
userId,
message,
createdAt,
sendAt: new Date(createdAt.getTime() + 60 * 60 * 1000),
};
await dependencies.reminders.add(reminder);
return reminder;
};
The test controls reality#
test("schedules the reminder one hour later", async () => {
const add = jest.fn();
const run = scheduleReminder({
clock: () => new Date("2026-01-10T09:00:00.000Z"),
generateId: () => "reminder-123",
reminders: { add },
});
await run("user-1", "Stand up");
expect(add).toHaveBeenCalledWith({
id: "reminder-123",
userId: "user-1",
message: "Stand up",
createdAt: new Date("2026-01-10T09:00:00.000Z"),
sendAt: new Date("2026-01-10T10:00:00.000Z"),
});
});
No fuzzy time comparison, global fake timer, or UUID matcher is required.
Production wiring remains simple:
const run = scheduleReminder({
clock: () => new Date(),
generateId: () => crypto.randomUUID(),
reminders: mongoReminderRepository,
});
Inject only nondeterminism that affects behavior. Wrapping every language function creates noise. Clocks, ID generators, randomness, and sometimes retry delays are strong candidates because tests need to control them.
This technique also exposes domain questions. Should expiration use request-start time or write time? Is an ID allocated before validation? Once time and identity are explicit inputs, those choices stop hiding inside implementation details.