# 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: 1. **Server-only modules** (`$lib/server/*`, `*.server.ts`) - Only run on server 2. **Universal modules** - Can run on both server and client 3. **Environment variables** - Different modules for static vs dynamic access ## Key Principles 1. **`vi.mock()` is hoisted** - Always executed before imports 2. **Use factory functions** - Return mocked implementations 3. **Mock before import** - Mocks must be defined before the module is imported 4. **Clean up** - Always restore/reset mocks in `beforeEach` or `afterEach` --- ## 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 ```typescript // 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 ```typescript 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 ```typescript 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: ```typescript 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 ```typescript // ❌ 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 ```typescript // ❌ 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 ```typescript // ✅ 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 ```typescript import { vi } from 'vitest'; // ✅ CORRECT - Type assertion const mockFn = vi.fn<() => Promise>(); mockFn.mockResolvedValue('test'); // OR use vi.mocked() import { vi, type Mock } from 'vitest'; const mockFn = vi.fn() as Mock<() => Promise>; ``` --- ## Testing Async Queue Processing ### Solution 1: Wait for Processing ```typescript 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 ```typescript 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 1. **Always mock before import** - `vi.mock()` calls are hoisted but still need to be before your imports 2. **Use factory functions** - Return new instances to avoid state leaking between tests 3. **Clean up thoroughly** - Use `beforeEach`/`afterEach` to reset state 4. **Type your mocks** - Use TypeScript generics for type-safe mocks 5. **Test isolation** - Each test should be independent 6. **Mock at the right level** - Mock external boundaries (HTTP, DB), not internal logic 7. **Use `vi.waitFor()`** - For async operations instead of arbitrary `setTimeout()` 8. **Snapshot complex mocks** - Use `expect.any(Function)` for callbacks 9. **Always await `.json()` on responses** - Both success and error responses --- ## Quick Reference: Mock Cheat Sheet ```typescript // 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 services - `src/tests/queue-processor.spec.ts` - Mocking config module and services - `src/tests/queue-api.spec.ts` - Testing API endpoints with proper async handling --- ## Additional Resources - [Vitest Mocking Guide](https://vitest.dev/guide/mocking.html) - [SvelteKit Testing](https://kit.svelte.dev/docs/testing) - [Vitest API Reference](https://vitest.dev/api/)