Server Testing Patterns
Overview
Server tests follow the Client-Server Alignment Strategy by using
real FormData and Request objects instead of heavy mocking. This
catches client-server contract mismatches that mocked tests miss.
For component testing patterns, see Testing Patterns. For setup and configuration, see Getting Started.
Essential Setup Pattern
1import { describe, expect, it, vi } from 'vitest';
2
3// Import your server handlers
4import { GET, POST } from './+server';
5import { actions } from './+page.server';Client-Server Alignment Strategy
The key principle: use real web APIs instead of mocking data structures.
Why Real Objects Matter
1// ❌ WRONG: Mocked objects miss field name mismatches
2const mock_form_data = {
3 get: (key: string) => mock_values[key],
4};
5
6// ✅ CORRECT: Real FormData catches field name issues
7const form_data = new FormData();
8form_data.append('email', '[email protected]');
9form_data.append('password', 'secure123');Real objects catch:
- Field name typos (
emailvse-mail) - Missing required fields
- Type mismatches (string vs number)
- Content-Type header issues
- URL parameter encoding problems
API Route Pattern
GET Requests
1describe('API Route', () => {
2 it('should handle GET requests', async () => {
3 // ✅ Real Request object - catches URL/header issues
4 const request = new Request('http://localhost/api/todos');
5 const response = await GET({ request });
6
7 expect(response.status).toBe(200);
8
9 const data = await response.json();
10 expect(data).toHaveProperty('todos');
11 expect(Array.isArray(data.todos)).toBe(true);
12 });
13});POST Requests with JSON
1describe('API Route', () => {
2 it('should handle POST requests with validation', async () => {
3 // ✅ Real Request with JSON body - tests actual parsing
4 const request = new Request('http://localhost/api/todos', {
5 method: 'POST',
6 headers: { 'Content-Type': 'application/json' },
7 body: JSON.stringify({ title: 'New todo', completed: false }),
8 });
9
10 const response = await POST({ request });
11 expect(response.status).toBe(201);
12
13 const data = await response.json();
14 expect(data.todo.title).toBe('New todo');
15 });
16});Validation Errors
1describe('API Route', () => {
2 it('should handle validation errors', async () => {
3 const request = new Request('http://localhost/api/todos', {
4 method: 'POST',
5 headers: { 'Content-Type': 'application/json' },
6 body: JSON.stringify({ title: '' }), // Invalid data
7 });
8
9 const response = await POST({ request });
10 expect(response.status).toBe(400);
11
12 const data = await response.json();
13 expect(data.error).toContain('Title is required');
14 });
15});Authentication
1describe('API Route', () => {
2 it('should handle authentication', async () => {
3 const request = new Request('http://localhost/api/secure-data', {
4 headers: { Authorization: 'Bearer valid-token' },
5 });
6
7 const response = await GET({ request });
8 expect(response.status).toBe(200);
9 });
10});FormData Submissions
1describe('API Route', () => {
2 it('should handle FormData submissions', async () => {
3 // ✅ Real FormData - catches field name mismatches
4 const form_data = new FormData();
5 form_data.append('email', '[email protected]');
6 form_data.append('password', 'secure123');
7
8 const request = new Request('http://localhost/api/register', {
9 method: 'POST',
10 body: form_data,
11 });
12
13 // Only mock external services, not data structures
14 vi.mocked(database.users.create).mockResolvedValue({
15 id: '123',
16 email: '[email protected]',
17 });
18
19 const response = await POST({ request });
20 expect(response.status).toBe(201);
21
22 const data = await response.json();
23 expect(data.user.email).toBe('[email protected]');
24 });
25});Server Hook Pattern
1describe('Server Hooks', () => {
2 it('should add security headers', async () => {
3 const event = create_mock_event('GET', '/');
4 const response = await handle({ event, resolve: mock_resolve });
5
6 expect(response.headers.get('X-Content-Type-Options')).toBe(
7 'nosniff',
8 );
9 expect(response.headers.get('X-Frame-Options')).toBe(
10 'SAMEORIGIN',
11 );
12 expect(response.headers.get('X-XSS-Protection')).toBe(
13 '1; mode=block',
14 );
15 });
16
17 it('should handle authentication', async () => {
18 const event = create_mock_event('GET', '/protected', {
19 cookies: { session: 'invalid-session' },
20 });
21
22 const response = await handle({ event, resolve: mock_resolve });
23 expect(response.status).toBe(302);
24 expect(response.headers.get('Location')).toBe('/login');
25 });
26});Form Actions Pattern
SvelteKit form actions use the same real-object approach:
1describe('Form Actions', () => {
2 it('should handle login action', async () => {
3 const form_data = new FormData();
4 form_data.append('email', '[email protected]');
5 form_data.append('password', 'password123');
6
7 const request = new Request('http://localhost/login', {
8 method: 'POST',
9 body: form_data,
10 });
11
12 const result = await actions.login({
13 request,
14 cookies: mock_cookies,
15 locals: {},
16 });
17
18 expect(result).not.toHaveProperty('error');
19 });
20
21 it('should return validation errors', async () => {
22 const form_data = new FormData();
23 form_data.append('email', 'invalid-email');
24 form_data.append('password', '');
25
26 const request = new Request('http://localhost/login', {
27 method: 'POST',
28 body: form_data,
29 });
30
31 const result = await actions.login({
32 request,
33 cookies: mock_cookies,
34 locals: {},
35 });
36
37 expect(result).toHaveProperty('error');
38 expect(result.errors.email).toBe('Invalid email format');
39 expect(result.errors.password).toBe('Password is required');
40 });
41});Mocking Strategy
What to Mock
- External APIs and services
- Database calls
- Authentication providers
- Email services
- File system operations
What NOT to Mock
FormDataobjectsRequestobjectsResponseobjects- URL parsing
- Headers
Example: Minimal Mocking
1describe('User Registration', () => {
2 it('should register new user', async () => {
3 // ✅ Real FormData
4 const form_data = new FormData();
5 form_data.append('email', '[email protected]');
6 form_data.append('password', 'SecurePass123');
7 form_data.append('name', 'New User');
8
9 // ✅ Real Request
10 const request = new Request('http://localhost/api/register', {
11 method: 'POST',
12 body: form_data,
13 });
14
15 // ✅ Only mock external service
16 vi.mocked(database.users.create).mockResolvedValue({
17 id: 'user-123',
18 email: '[email protected]',
19 name: 'New User',
20 });
21
22 vi.mocked(email_service.send_welcome).mockResolvedValue(true);
23
24 const response = await POST({ request });
25 expect(response.status).toBe(201);
26 });
27});Helper Functions
Creating Mock Events
1function create_mock_event(
2 method: string,
3 path: string,
4 options?: {
5 cookies?: Record<string, string>;
6 headers?: Record<string, string>;
7 body?: FormData | string;
8 },
9) {
10 const url = new URL(path, 'http://localhost');
11
12 return {
13 request: new Request(url, {
14 method,
15 headers: options?.headers,
16 body: options?.body,
17 }),
18 cookies: {
19 get: (name: string) => options?.cookies?.[name],
20 set: vi.fn(),
21 delete: vi.fn(),
22 },
23 locals: {},
24 url,
25 params: {},
26 };
27}Mock Resolve Function
1const mock_resolve = async (event: any) => {
2 return new Response('OK', { status: 200 });
3};Quick Reference
Essential Patterns Checklist
- ✅ Use real
FormDataobjects - never mock data access - ✅ Use real
Requestobjects - catches URL/header issues - ✅ Only mock external services (database, email, APIs)
- ✅ Test validation errors with invalid data
- ✅ Test authentication with valid and invalid tokens
- ✅ Verify response status codes
- ✅ Check response body structure
Common Fixes
- Field not found: Check field names match between client and server
- Parsing errors: Verify Content-Type header matches body format
- Auth failures: Ensure Authorization header format is correct
- Status code wrong: Check error handling returns proper codes