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 (email vs e-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

  • FormData objects
  • Request objects
  • Response objects
  • 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 FormData objects - never mock data access
  • ✅ Use real Request objects - 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