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

Mocking External Services

The external dependency problem

Your API sends emails (confirmation, password reset), calls payment services (Stripe), stores files (S3), and calls third-party APIs. Tests should not: send real emails, charge real credit cards, upload real files, or depend on external services being available.

Mocking replaces a real service with a fake that records calls and returns predetermined responses.

Simple function mocking

The simplest mock: replace the function with one that records calls:

// src/services/email.ts
export async function sendEmail(to: string, subject: string, body: string): Promise<void> {
  // Real implementation: calls an email API
  await fetch("https://api.emailservice.com/send", {
    /* ... */
  });
}
// tests/helpers/mocks.ts
export function createEmailMock() {
  const calls: Array<{ to: string; subject: string; body: string }> = [];

  return {
    sendEmail: async (to: string, subject: string, body: string) => {
      calls.push({ to, subject, body });
    },
    getCalls: () => calls,
    getLastCall: () => calls[calls.length - 1],
    reset: () => {
      calls.length = 0;
    },
  };
}

The mock records every call. Tests check: was the email “sent”? To whom? With what subject?

Using mocks with dependency injection

Pass the email service as a parameter (like the test database pattern from the Business Logic lesson):

// src/services/notifications.ts
import { sendEmail as realSendEmail } from "./email.js";

export function createNotificationService(sendEmail = realSendEmail) {
  return {
    async sendOrderConfirmation(email: string, orderId: string) {
      await sendEmail(email, "Order Confirmed", `Your order ${orderId} has been placed.`);
    },
    async sendPasswordReset(email: string, token: string) {
      await sendEmail(email, "Password Reset", `Reset your password: /reset?token=${token}`);
    },
  };
}

Production: createNotificationService() uses the real email function. Tests: createNotificationService(mock.sendEmail) uses the mock.

test("sendOrderConfirmation sends email with order ID", async () => {
  const mock = createEmailMock();
  const notifications = createNotificationService(mock.sendEmail);

  await notifications.sendOrderConfirmation("[email protected]", "order-1");

  expect(mock.getCalls()).toHaveLength(1);
  expect(mock.getLastCall().to).toBe("[email protected]");
  expect(mock.getLastCall().subject).toBe("Order Confirmed");
  expect(mock.getLastCall().body).toContain("order-1");
});

Vitest’s built-in mocking

Vitest provides vi.fn() for quick mocks:

import { vi, test, expect } from "vitest";

test("calls sendEmail with correct arguments", async () => {
  const mockSendEmail = vi.fn();
  const notifications = createNotificationService(mockSendEmail);

  await notifications.sendOrderConfirmation("[email protected]", "order-1");

  expect(mockSendEmail).toHaveBeenCalledOnce();
  expect(mockSendEmail).toHaveBeenCalledWith(
    "[email protected]",
    "Order Confirmed",
    expect.stringContaining("order-1"),
  );
});

vi.fn() creates a mock function that tracks calls. toHaveBeenCalledWith checks the arguments.

Mocking services that return data

Some mocks need to return data, not just record calls:

const mockPayment = {
  charge: vi.fn().mockResolvedValue({ id: "charge-1", status: "succeeded" }),
};

test("processes payment and creates order", async () => {
  const result = await processOrder(orderData, mockPayment);

  expect(mockPayment.charge).toHaveBeenCalledWith(29.99, "tok_test");
  expect(result.status).toBe("paid");
});

mockResolvedValue makes the mock return a Promise that resolves to the given value.

Mocking failed services

test("handles payment failure gracefully", async () => {
  const mockPayment = {
    charge: vi.fn().mockRejectedValue(new Error("Card declined")),
  };

  const result = await processOrder(orderData, mockPayment);

  expect(result.status).toBe("payment_failed");
  expect(result.error).toBe("Card declined");
});

[!NOTE] The Error Handling course built fallback patterns for when external services fail. Mocking lets you test those fallbacks without waiting for real failures.

Exercises

Exercise 1: Create an email mock. Test that order creation sends a confirmation email with the correct recipient and subject.

Exercise 2: Mock a payment service. Test success (charge succeeds) and failure (card declined).

Exercise 3: Use vi.fn() to mock a service. Assert it was called with specific arguments.

Why mock external services in tests?

← Request Helpers Testing Background Jobs →

© 2026 hectoday. All rights reserved.