hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Testing APIs with @hectoday/http

Why Test

  • What Testing Gives You
  • Types of Tests
  • Project Setup

Unit Testing

  • Testing Pure Functions
  • Testing Zod Schemas
  • Testing Business Logic

Integration Testing

  • Testing Route Handlers
  • Testing GET Endpoints
  • Testing POST Endpoints
  • Testing Error Responses
  • Testing Authentication

Test Helpers

  • Factories and Fixtures
  • Test Database Isolation
  • Request Helpers

Advanced Testing

  • Mocking External Services
  • Testing Background Jobs
  • Testing Edge Cases

Putting It All Together

  • Test Organization
  • Checklist and Capstone

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?

← Mocking External Services Testing Edge Cases →

© 2026 hectoday. All rights reserved.