Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who had just finished the integration for our billing system. He’d written a great suite of tests, but there was one problem: he had forgotten to mock the Stripe API. When our CI/CD pipeline ran, it triggered the actual payment logic. By the time we noticed the logs, he had accidentally charged a test credit card several hundred times and triggered a fraud alert that temporarily froze our production account. It was a frantic hour of phone calls and apology emails.

That’s the danger of "side effects." When your code reaches out to a database, an external API, or the file system, your tests are no longer isolated. They become slow, flaky, and—as my colleague found out—potentially expensive. This is where mocking and spying come in. They allow us to replace the "real" world with a controlled simulation so we can test our logic without actually sending an email or charging a customer.

Taming the External World with Mocks

A mock is essentially a fake version of a dependency. Instead of calling the real PaymentGateway.charge(), which hits a network endpoint, you provide a mock function that says, "I don't actually do anything, but I'll return { success: true } every time you call me."

I like to think of mocks as stunt doubles. They look like the real actor to the rest of the cast, but they're there to take the hit so the lead actor (your production code) doesn't get hurt. In JavaScript, especially when using frameworks like Jest or Vitest, you can replace a whole module or a specific function with a mock. This ensures your tests stay fast and deterministic.

// The real service we want to avoid calling in tests
const EmailService = {
  sendWelcomeEmail: async (email) => {
    // Imagine a slow network request to an SMTP server here
    console.log(`Sending real email to ${email}...`);
    return { status: 200 };
  }
};

// Our business logic
async function registerUser(user, mailer) {
  // ... user creation logic ...
  await mailer.sendWelcomeEmail(user.email);
  return { message: "User registered!" };
}

// In our test, we mock the mailer
const mockMailer = {
  sendWelcomeEmail: jest.fn().mockResolvedValue({ status: 200 })
};

const result = await registerUser({ email: 'dev@example.com' }, mockMailer);
// The real EmailService was never touched. No emails were sent.

Double-Checking the Plumbing with Spies

While a mock replaces behavior, a spy is more about observation. A spy lets you track how a function was called without necessarily changing what it does. You use a spy when you don't care about the result of the function as much as you care that the function was actually triggered.

I've seen developers use mocks for everything, but sometimes that's overkill. If you have a function that logs errors to a console or a monitoring service, you don't necessarily need to change how it works; you just need to be 100% sure that when an error happens, the logger is called with the right message. That's a job for a spy.

When you spy on a function, you can ask questions like: "Was this called exactly once?" or "Was it called with the specific UserID I expected?" It’s the difference between replacing a pipe (mocking) and putting a flow-meter on the pipe to see if water is moving (spying).

const Logger = {
  logError: (msg) => console.error(msg)
};

const spy = jest.spyOn(Logger, 'logError');

try {
  throw new Error("Database connection failed");
} catch (e) {
  Logger.logError(e.message);
}

// We aren't changing the logger, we're just verifying it happened
expect(spy).toHaveBeenCalledWith("Database connection failed");
expect(spy).toHaveBeenCalledTimes(1);

spy.mockRestore(); // Clean up after yourself!



📋 Practical Task

Exercise: The Order Confirmation Guard

You are working on an e-commerce checkout system. You have a function processOrder that depends on two external services: PaymentProcessor (which handles the money) and AnalyticsTracker (which logs the sale for the marketing team).

Your Goal: Write a test suite that ensures the order process works correctly without making real API calls.

Requirements:

  • Create a mock for PaymentProcessor.charge that returns a resolved promise with { success: true }.
  • Use a spy on AnalyticsTracker.trackEvent to verify that the event "order_completed" is sent exactly once when the payment is successful.
  • Ensure that if the PaymentProcessor returns { success: false }, the AnalyticsTracker is not called.
// Provided Code
const PaymentProcessor = {
  charge: async (amount) => { 
    /* Real API Call */ 
    throw new Error("Do not call this in tests!"); 
  }
};

const AnalyticsTracker = {
  trackEvent: (eventName) => { 
    /* Real API Call */ 
    console.log(`Tracking: ${eventName}`); 
  }
};

async function processOrder(amount) {
  const response = await PaymentProcessor.charge(amount);
  if (response.success) {
    AnalyticsTracker.trackEvent("order_completed");
    return "Order Successful";
  }
  return "Order Failed";
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.