When to mock dependencies and when to spy on behavior for effective testing.
Testing is the backbone of reliable software, but choosing the right tools for the job can be tricky. In Vitest with TypeScript, two powerful utilities, mock
and spyOn
, often cause confusion. Let’s break down their use cases, differences, and best practices to help you write better unit and integration tests.
What’s the Difference?
At a glance:
-
mock
replaces a function/module’s implementation entirely. -
spyOn
observes a function’s behavior without changing it (unless you tell it to).
When to Use mock
Use vi.mock
to isolate your code from external dependencies by replacing their implementations.
Use Cases
- Unit Testing Mock dependencies (e.g., APIs, databases) to test a single component in isolation.
// Mock an API client
vi.mock('@/services/api', () => ({
fetchData: vi.fn().mockResolvedValue({ data: 'fake' }),
}));
test('displays mocked data', async () => {
render(<DataFetcher />);
await waitFor(() => expect(screen.getByText('fake')).toBeVisible());
});
Avoid Side Effects
Prevent network calls, file system operations, or other costly actions.Simulate Edge Cases
Force errors or unusual responses (e.g., 500 status codes).
When to Use spyOn
Use vi.spyOn
to track function calls and arguments while preserving the original behavior.
Use Cases
- Integration Testing Verify interactions between modules without breaking their real behavior.
import * as analytics from '@/utils/analytics';
test('logs an event on button click', () => {
const trackSpy = vi.spyOn(analytics, 'trackEvent');
render(<SignupButton />);
userEvent.click(screen.getByText('Sign Up'));
expect(trackSpy).toHaveBeenCalledWith('signup_clicked');
});
Assert Function Behavior
Check if a function was called, how many times, or with specific arguments.Temporary Overrides
Mock a function for a single test, then restore it:
const spy = vi.spyOn(validator, 'isValid').mockReturnValue(false);
// Test invalid case
spy.mockRestore(); // Restore original
Unit Tests vs. Integration Tests
Test Type | Mock (vi.mock ) |
Spy (vi.spyOn ) |
---|---|---|
Unit Tests | Mock all dependencies to isolate code. | Rarely used. Check indirect side effects. |
Integration | Mock external services (e.g., APIs). | Spy on internal logic (e.g., logging). |
Key Differences
Feature | mock |
spyOn |
---|---|---|
Implementation | Replaces the original function. | Retains original behavior by default. |
Scope | Applies to entire modules or functions. | Works on individual object methods. |
Cleanup | Auto-reset with Vitest config. | Manually restore with mockRestore() . |
Combining Mocks and Spies
In complex scenarios, mix both tools:
// Mock an external API
vi.mock('@/services/payment', () => ({
processPayment: vi.fn().mockResolvedValue({ success: true }),
}));
// Spy on an internal validator
const validateSpy = vi.spyOn(validator, 'validateCard');
test('processes payment with valid card', async () => {
await submitPayment({ card: '4111 1111 1111 1111' });
expect(validateSpy).toHaveReturnedWith(true);
expect(processPayment).toHaveBeenCalled();
});
TypeScript Pro Tips
-
Type-Safe Mocks
Use
MockedFunction
orMockedObject
for better type inference:
import type { fetchData } from '@/services/api';
vi.mock('@/services/api');
const mockedFetch = vi.mocked(fetchData); // Type-safe mock!
- Spy Assertions Add type assertions if needed:
const spy = vi.spyOn(console, 'log') as MockedFunction<typeof console.log>;
Final Advice
- Unit Tests: Mock aggressively to isolate components.
- Integration Tests: Spy to validate interactions, mock only externals.
-
Always Restore Spies: Use
afterEach(() => vi.restoreAllMocks())
to avoid test pollution.
By mastering mock
and spyOn
, you’ll write tests that are faster, more reliable, and easier to debug. Happy testing! 🚀
Got questions? Drop them in the comments below!
Follow for more Vitest and TypeScript tips!
Top comments (0)