Testing Background Jobs
Two things to test
Background jobs (from the Background Jobs course) have two testable parts: enqueueing (was the job added to the queue?) and handling (does the job do the right thing when processed?).
Testing that jobs are enqueued
When a user places an order, the handler should enqueue a confirmation email job. Test this by mocking the enqueue function:
import { vi, test, expect } from "vitest";
test("POST /v2/orders enqueues confirmation email", async () => {
const mockEnqueue = vi.fn();
// Inject mockEnqueue into the order handler (via dependency injection)
const response = await app.fetch(
post(
"/v2/orders",
{
items: [{ productId: "prod-1", quantity: 1 }],
paymentToken: "tok_test",
},
authHeader("u1"),
),
);
expect(response.status).toBe(201);
expect(mockEnqueue).toHaveBeenCalledWith(
"send_order_confirmation",
expect.objectContaining({ orderId: expect.any(String) }),
);
}); The test verifies: the order was created (201) AND the email job was enqueued. It does not test whether the email is actually sent — that is the job handler’s responsibility.
Testing job handlers directly
Job handlers are functions that take a payload and do work. Test them like any other function:
import { processJob } from "../../src/job-handlers.js";
describe("send_order_confirmation handler", () => {
test("sends email to the order's user", async () => {
const order = createOrder({ userId: "u1" });
const user = createUser({ id: "u1", email: "[email protected]" });
const mockEmail = createEmailMock();
await processJob(
{
type: "send_order_confirmation",
payload: JSON.stringify({ orderId: order.id }),
},
{ sendEmail: mockEmail.sendEmail },
);
expect(mockEmail.getLastCall().to).toBe("[email protected]");
expect(mockEmail.getLastCall().subject).toContain("Order Confirmed");
});
test("throws for nonexistent order", async () => {
await expect(
processJob({
type: "send_order_confirmation",
payload: JSON.stringify({ orderId: "nonexistent" }),
}),
).rejects.toThrow();
});
}); The handler is tested with a real database (test database) and a mocked email service. This catches bugs in the handler’s logic without sending real emails.
Testing the full job lifecycle
test("order → enqueue → process → email sent", async () => {
const user = createUser({ email: "[email protected]" });
const mockEmail = createEmailMock();
// 1. Create order (enqueues job)
const orderRes = await app.fetch(post("/v2/orders", orderData, authHeader(user.id)));
expect(orderRes.status).toBe(201);
// 2. Check job was enqueued
const pendingJobs = testDb.prepare("SELECT * FROM jobs WHERE status = 'pending'").all();
expect(pendingJobs).toHaveLength(1);
expect((pendingJobs[0] as any).type).toBe("send_order_confirmation");
// 3. Process the job
const job = pendingJobs[0] as any;
await processJob(job, { sendEmail: mockEmail.sendEmail });
// 4. Verify email
expect(mockEmail.getLastCall().to).toBe("[email protected]");
}); This integration test covers the full flow: HTTP request → job enqueue → job processing → side effect. Each step is verified.
Testing idempotent handlers
test("processing the same job twice sends email only once", async () => {
const mockEmail = createEmailMock();
await processJob(job, { sendEmail: mockEmail.sendEmail });
await processJob(job, { sendEmail: mockEmail.sendEmail });
expect(mockEmail.getCalls()).toHaveLength(1); // idempotent!
}); [!NOTE] The Background Jobs course’s Idempotent Jobs lesson explained why handlers must be safe to repeat. This test verifies that property.
Exercises
Exercise 1: Mock the enqueue function. Test that creating an order enqueues the correct job type and payload.
Exercise 2: Test a job handler directly. Mock the email service. Verify the email is sent to the right address.
Exercise 3: Test idempotency: process a job twice, verify the side effect happens once.
Why test job enqueueing and job handling separately?