Mocking is the technique that makes unit testing possible: instead of hitting a real database or calling a real payment API in every test, you replace the dependency with a controlled stand-in. The trouble is that the word "mock" is used for three distinct things — mocks, stubs, and fakes — each with different trade-offs. Using the wrong one is the most common reason test suites become fragile and meaningless.
What changed in 2026
- Vitest's
vi.mock() and vi.spyOn() became the standard for JavaScript, replacing Jest in most new projects while keeping a compatible API.
- Python's
unittest.mock remains the standard; pytest-mock wraps it with a cleaner fixture API.
- Over-mocking backlash grew. The community converged on "mock at the boundary" and "prefer fakes" after years of brittle mock-heavy test suites.
- TypeScript strict types mean mock objects that do not fully implement an interface fail at compile time — caught earlier.
The three test doubles
| Name |
What it does |
When to use |
| Stub |
Returns canned data; no assertions |
"Give me a user" — you just need the value |
| Mock |
Records calls; verifies interactions |
"Assert that sendEmail was called once" |
| Fake |
Working lightweight implementation |
Complex collaborators used in many tests |
// Stub — returns hardcoded data
const userRepo = { findById: async () => ({ id: "1", name: "Alice" }) };
// Mock — verifies a call happened
const emailService = { send: vi.fn() };
// later: expect(emailService.send).toHaveBeenCalledOnce();
// Fake — real logic, no real I/O
class InMemoryUserRepository implements UserRepository {
private store = new Map<string, User>();
async findById(id: string) { return this.store.get(id) ?? null; }
async save(user: User) { this.store.set(user.id, user); }
}
Mock at the boundary
The golden rule: mock the line where your code meets the outside world.
Your code → [boundary] → Outside world
↑ mock here
Boundary examples: HTTP clients, email senders, payment gateways, file system, clocks/timers. Non-boundary examples: your own service classes, domain objects, pure functions.
If you mock your own OrderService to test CheckoutService, you are testing a mock's behaviour, not real integration. Use a real OrderService (or a fake) in that test.
When to use each
// Use a STUB when the test doesn't care about the call, just the return value
it("formats the user name", async () => {
const repo = { findById: async () => ({ id: "1", name: "alice" }) };
const svc = new UserService(repo);
expect(await svc.displayName("1")).toBe("Alice");
});
// Use a MOCK when you need to assert the call happened
it("sends a welcome email on signup", async () => {
const emailer = { send: vi.fn() };
const svc = new AuthService(emailer);
await svc.signup("a@example.com");
expect(emailer.send).toHaveBeenCalledWith(
expect.objectContaining({ to: "a@example.com", template: "welcome" })
);
});
// Use a FAKE when the collaborator is complex and used in many tests
const repo = new InMemoryUserRepository();
Python example
from unittest.mock import MagicMock, call
from myapp.services import NotificationService
def test_notify_sends_sms_and_email():
sms = MagicMock()
mail = MagicMock()
svc = NotificationService(sms_client=sms, mail_client=mail)
svc.notify(user_id="u1", message="Hello")
sms.send.assert_called_once_with(user_id="u1", text="Hello")
mail.send.assert_called_once()
How to pick
- External service call (HTTP, email, SMS, payment) — mock or stub at the client boundary.
- Clock / random number — replace with a controllable fake (clock that you advance manually).
- Complex stateful collaborator used across many tests — write an in-memory fake.
- Simple one-off data return — use a plain stub object.
- Database access in unit tests — use an in-memory fake repository.
- Database access in integration tests — use a real database (Testcontainers).
Common mistakes
Mocking your own internal classes. When ServiceA is tested by mocking ServiceB, you miss every bug in their interaction.
Verifying implementation details. Asserting privateHelper was called three times. When you inline that helper, the test breaks despite no behaviour change.
Over-specifying mock calls. Asserting the exact arguments including defaults and timestamps that change. Use expect.objectContaining and expect.any(Date).
Forgetting to reset mocks. Mock state that bleeds between tests causes order-dependent failures. Use vi.clearAllMocks() in beforeEach, or autoClearMocks: true in config.
What to skip
- Mocking the database in integration tests — defeats their purpose. See Integration testing explained in 2026.
- Mocking pure functions — they have no side effects; just call them.
- Complex mock setup that mirrors the real implementation — that is a sign you need a real fake or a real dependency.
FAQ
What is the difference between a spy and a mock?
A spy wraps the real implementation and records calls; a mock replaces it entirely. vi.spyOn creates a spy; vi.fn() creates a mock.
Should I use an auto-mocking library?
Auto-mockers generate mock objects from interfaces automatically. They are convenient for large interfaces but can generate over-specified tests. Use them where they save real time.
How do I mock a module import?
Vitest: vi.mock('./module'). Jest: jest.mock('./module'). Python: unittest.mock.patch('myapp.module.ClassName').
When is over-mocking a sign of bad design?
When you need 8 mocks to test one method, the method has too many dependencies. Consider splitting responsibilities.
Where to go next
See Unit testing explained in 2026, Integration testing explained in 2026, and Dependency injection explained in 2026.