Advanced Testing Patterns
Mocking, Stubs, and Test Doubles
Isolate units under test from their dependencies using mocks, spies, stubs, and fakes.
Why Mock?
Unit tests must be fast, reliable, and isolated. A test that hits a real database, calls a real API, or reads real files is not a unit test — it's an integration test, and it will be slow and flaky.
Mocking replaces real dependencies with controlled substitutes that behave predictably.
Taxonomy of Test Doubles
Dummy — Passed as argument but never used.
Stub — Returns predetermined data. "When called with X, return Y."
Spy — Records how it was called without changing behavior.
Mock — A stub that also verifies expected interactions. Fails the test if not called correctly.
Fake — A simplified working implementation (in-memory database, fake email sender).
Vitest Mocking
Use vi.fn() to create mock functions. Use .mockReturnValue() for stubs, .mockResolvedValue() for async stubs, and .mockRejectedValue() for error stubs. Assert with toHaveBeenCalledWith() and toHaveBeenCalledTimes().
Mocking Modules
Use vi.mock() to replace entire modules. Import the mocked module and use vi.mocked() to access mock methods with proper TypeScript typing.
Spying on Existing Methods
Use vi.spyOn() to monitor existing functions without replacing them. Always call spy.mockRestore() after the test to prevent mock state from leaking.
When NOT to Mock
Over-mocking is a common anti-pattern. If everything is mocked, the test verifies the mocks, not the real behavior.
Don't mock: pure functions, simple data objects, your own code's internal logic.
Do mock: external APIs, database queries (in unit tests), file system, email/SMS services, Date.now() and Math.random() for determinism.
Key Takeaways
- vi.fn() creates mock functions; .mockReturnValue() and .mockResolvedValue() control what they return
- vi.mock() replaces entire modules — use for external services, databases, and email senders
- Spies record calls without changing behavior; use vi.spyOn() for monitoring existing functions
- Over-mocking creates tests that don't verify real behavior — only mock external dependencies
- Always restore spies after tests to prevent mock state from leaking between tests
Example
// UserService tests with mocked dependencies