JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
122: Mocking and Spies
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.chargethat returns a resolved promise with{ success: true }. - Use a spy on
AnalyticsTracker.trackEventto verify that the event"order_completed"is sent exactly once when the payment is successful. - Ensure that if the
PaymentProcessorreturns{ success: false }, theAnalyticsTrackeris 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";
}
There are no comments for now.