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?