8.2 KiB
8.2 KiB
Vitest Mocking Guide for SvelteKit
This guide explains how to properly mock dependencies when testing SvelteKit applications with Vitest.
Understanding Mocking in SvelteKit Context
SvelteKit has a unique architecture where code can run on both server and client. This affects how we mock:
- Server-only modules (
$lib/server/*,*.server.ts) - Only run on server - Universal modules - Can run on both server and client
- Environment variables - Different modules for static vs dynamic access
Key Principles
vi.mock()is hoisted - Always executed before imports- Use factory functions - Return mocked implementations
- Mock before import - Mocks must be defined before the module is imported
- Clean up - Always restore/reset mocks in
beforeEachorafterEach
Mocking Environment Variables ($env/dynamic/private)
Problem: Can't directly mock $env/dynamic/private because it's a SvelteKit magic module.
Solution: Create a config module that wraps env access, then mock the config.
Example: Queue Config Module
// src/lib/server/queue/config.ts
import { env } from '$env/dynamic/private';
export const queueConfig = {
concurrency: parseInt(env.QUEUE_CONCURRENCY || '2', 10),
maxRetries: parseInt(env.QUEUE_MAX_RETRIES || '3', 10),
tandoor: {
enabled: !!env.TANDOOR_TOKEN,
token: env.TANDOOR_TOKEN || null
}
};
Mocking the Config in Tests
import { vi, beforeEach, afterEach } from 'vitest';
import * as queueConfigModule from '$lib/server/queue/config';
// Mock the config module
vi.mock('$lib/server/queue/config', () => ({
queueConfig: {
concurrency: 2,
maxRetries: 3,
tandoor: { enabled: true, token: 'test-token' }
}
}));
describe('QueueProcessor', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
});
Mocking External Service Modules
Recommended Approach: Mock Entire Module
import { vi } from 'vitest';
// IMPORTANT: Mock BEFORE importing the module that uses it
vi.mock('$lib/server/extraction', () => ({
extractTextAndThumbnail: vi.fn().mockResolvedValue({
bodyText: 'Mock recipe text',
thumbnail: 'https://mock.com/image.jpg'
})
}));
// NOW import the module that depends on these
import { queueProcessor } from '$lib/server/queue/QueueProcessor';
import { extractTextAndThumbnail } from '$lib/server/extraction';
describe('QueueProcessor', () => {
it('should use mocked services', async () => {
const item = queueManager.enqueue('https://instagram.com/p/test');
// Verify mock was called
expect(extractTextAndThumbnail).toHaveBeenCalledWith(
'https://instagram.com/p/test',
expect.any(Function)
);
});
});
Mocking API Endpoints (SvelteKit RequestHandler)
When testing API endpoints, be aware that error responses must be properly awaited:
import { describe, it, expect } from 'vitest';
import { POST } from '../routes/api/queue/+server';
describe('POST /api/queue', () => {
it('should reject invalid URLs', async () => {
const request = new Request('http://localhost/api/queue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'invalid-url' })
});
const response = await POST({ request } as any);
// ✅ CORRECT - Check status first
expect(response.status).toBe(400);
// ✅ CORRECT - Properly await error response
const data = await response.json();
expect(data.message).toContain('Invalid');
});
});
Common Pitfall: Not Awaiting Error Responses
// ❌ WRONG - This will fail
it('should reject invalid input', async () => {
const response = await endpoint({ request } as any);
const data = response.json(); // Missing await!
expect(data.message).toBe('Error'); // data is a Promise
});
// ✅ CORRECT
it('should reject invalid input', async () => {
const response = await endpoint({ request } as any);
expect(response.status).toBe(400);
const data = await response.json(); // Properly awaited
expect(data.message).toBe('Error');
});
Common Pitfalls and Solutions
Problem 1: Mock Not Working
// ❌ WRONG - Import before mock
import { queueProcessor } from './QueueProcessor';
vi.mock('./extraction');
// ✅ CORRECT - Mock before import
vi.mock('./extraction');
import { queueProcessor } from './QueueProcessor';
Problem 2: Mocks Not Resetting Between Tests
// ✅ SOLUTION - Always clean up
import { beforeEach, afterEach } from 'vitest';
beforeEach(() => {
vi.clearAllMocks(); // Clear call history
});
afterEach(() => {
vi.restoreAllMocks(); // Restore original implementations
});
Problem 3: TypeScript Errors with Mocked Functions
import { vi } from 'vitest';
// ✅ CORRECT - Type assertion
const mockFn = vi.fn<() => Promise<string>>();
mockFn.mockResolvedValue('test');
// OR use vi.mocked()
import { vi, type Mock } from 'vitest';
const mockFn = vi.fn() as Mock<() => Promise<string>>;
Testing Async Queue Processing
Solution 1: Wait for Processing
it('should process item', async () => {
const item = queueManager.enqueue('https://instagram.com/p/test');
// Wait for processing with timeout
await vi.waitFor(
() => {
const updated = queueManager.get(item.id);
expect(updated?.status).toBe('success');
},
{ timeout: 5000, interval: 100 }
);
});
Solution 2: Use Fake Timers
import { vi } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should process after delay', async () => {
queueManager.enqueue('https://test.com');
// Fast-forward time
await vi.advanceTimersByTimeAsync(1000);
// Now check results
});
Best Practices for SvelteKit + Vitest
- Always mock before import -
vi.mock()calls are hoisted but still need to be before your imports - Use factory functions - Return new instances to avoid state leaking between tests
- Clean up thoroughly - Use
beforeEach/afterEachto reset state - Type your mocks - Use TypeScript generics for type-safe mocks
- Test isolation - Each test should be independent
- Mock at the right level - Mock external boundaries (HTTP, DB), not internal logic
- Use
vi.waitFor()- For async operations instead of arbitrarysetTimeout() - Snapshot complex mocks - Use
expect.any(Function)for callbacks - Always await
.json()on responses - Both success and error responses
Quick Reference: Mock Cheat Sheet
// Mock entire module
vi.mock('./module', () => ({ export: vi.fn() }));
// Mock with factory
vi.mock('./module', () => {
return { dynamicExport: () => 'value' };
});
// Spy on existing export
vi.spyOn(module, 'export').mockReturnValue('value');
// Mock return value
mockFn.mockReturnValue('sync value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('async error'));
// Mock implementation
mockFn.mockImplementation((arg) => arg * 2);
mockFn.mockImplementationOnce((arg) => arg * 3);
// Check calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('arg');
// Reset/restore
vi.clearAllMocks(); // Clear call history
vi.resetAllMocks(); // + Reset implementations
vi.restoreAllMocks(); // + Restore original implementations
// Environment variables
vi.stubEnv('VAR_NAME', 'value');
vi.unstubAllEnvs();
// Timers
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
await vi.advanceTimersByTimeAsync(1000);
vi.useRealTimers();
// Async helpers
await vi.waitFor(() => expect(condition).toBe(true));
await vi.waitUntil(() => condition === true);
Examples from This Project
See the following test files for real-world examples:
src/tests/queue-manager.spec.ts- Mocking external servicessrc/tests/queue-processor.spec.ts- Mocking config module and servicessrc/tests/queue-api.spec.ts- Testing API endpoints with proper async handling