Three Levels of Confidence
Calm systems are testable systems. When data flows through load functions and components have explicit props, testing becomes straightforward.
Three levels of testing serve different purposes:
- Unit tests — Test load functions and utilities in isolation
- Component tests — Test components render correctly
- E2E tests — Test complete user flows in a real browser
Unit Testing Load Functions
Load functions are just async functions. Test them like any other function.
// routes/projects/[id]/+page.server.ts
import { error } from '@sveltejs/kit'
import { db } from '$lib/server/db'
export async function load({ params, locals }) {
if (!locals.user) {
error(401, 'Unauthorized')
}
const project = await db.getProject(params.id)
if (!project) {
error(404, 'Project not found')
}
if (project.ownerId !== locals.user.id) {
error(403, 'Access denied')
}
return { project }
} // routes/projects/[id]/+page.server.test.ts
import { describe, it, expect, vi } from 'vitest'
import { load } from './+page.server'
// Mock the database
vi.mock('$lib/server/db', () => ({
db: {
getProject: vi.fn()
}
}))
import { db } from '$lib/server/db'
describe('project load function', () => {
const mockUser = { id: 'user-1', name: 'Alice' }
const mockProject = { id: 'proj-1', name: 'Test', ownerId: 'user-1' }
it('returns project for authorized user', async () => {
vi.mocked(db.getProject).mockResolvedValue(mockProject)
const result = await load({
params: { id: 'proj-1' },
locals: { user: mockUser }
} as any)
expect(result.project).toEqual(mockProject)
})
it('throws 401 when not authenticated', async () => {
await expect(
load({
params: { id: 'proj-1' },
locals: { user: null }
} as any)
).rejects.toMatchObject({ status: 401 })
})
it('throws 404 when project not found', async () => {
vi.mocked(db.getProject).mockResolvedValue(null)
await expect(
load({
params: { id: 'nonexistent' },
locals: { user: mockUser }
} as any)
).rejects.toMatchObject({ status: 404 })
})
it('throws 403 when user lacks access', async () => {
vi.mocked(db.getProject).mockResolvedValue({
...mockProject,
ownerId: 'other-user'
})
await expect(
load({
params: { id: 'proj-1' },
locals: { user: mockUser }
} as any)
).rejects.toMatchObject({ status: 403 })
})
}) Load functions are pure—same inputs, same outputs. This makes them easy to test without rendering anything.
Component Testing
Use @testing-library/svelte to test components:
npm install -D @testing-library/svelte @testing-library/jest-dom <!-- lib/components/TaskCard.svelte -->
<script>
let { task, oncomplete, ondelete } = $props()
</script>
<div class="task" class:completed={task.completed}>
<button
class="toggle"
onclick={() => oncomplete?.(task.id)}
aria-label={task.completed ? 'Mark incomplete' : 'Mark complete'}
>
{task.completed ? '✓' : '○'}
</button>
<span class="title">{task.title}</span>
<button class="delete" onclick={() => ondelete?.(task.id)} aria-label="Delete task"> × </button>
</div> // lib/components/TaskCard.test.ts
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/svelte'
import userEvent from '@testing-library/user-event'
import TaskCard from './TaskCard.svelte'
describe('TaskCard', () => {
const mockTask = {
id: '1',
title: 'Test task',
completed: false
}
it('renders task title', () => {
render(TaskCard, { props: { task: mockTask } })
expect(screen.getByText('Test task')).toBeInTheDocument()
})
it('shows incomplete state', () => {
render(TaskCard, { props: { task: mockTask } })
expect(screen.getByLabelText('Mark complete')).toBeInTheDocument()
})
it('shows completed state', () => {
render(TaskCard, {
props: { task: { ...mockTask, completed: true } }
})
expect(screen.getByLabelText('Mark incomplete')).toBeInTheDocument()
})
it('calls oncomplete when toggle clicked', async () => {
const user = userEvent.setup()
const oncomplete = vi.fn()
render(TaskCard, {
props: { task: mockTask, oncomplete }
})
await user.click(screen.getByLabelText('Mark complete'))
expect(oncomplete).toHaveBeenCalledWith('1')
})
it('calls ondelete when delete clicked', async () => {
const user = userEvent.setup()
const ondelete = vi.fn()
render(TaskCard, {
props: { task: mockTask, ondelete }
})
await user.click(screen.getByLabelText('Delete task'))
expect(ondelete).toHaveBeenCalledWith('1')
})
}) Test what users see and do, not implementation details.
E2E Testing with Playwright
For complete user flows, use Playwright:
npm install -D @playwright/test
npx playwright install // tests/tasks.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Tasks', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login')
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('input[name="password"]', 'password')
await page.click('button[type="submit"]')
await page.waitForURL('/dashboard')
})
test('can create a task', async ({ page }) => {
await page.goto('/tasks')
// Open form
await page.click('text=New Task')
// Fill and submit
await page.fill('input[name="title"]', 'Buy groceries')
await page.fill('textarea[name="description"]', 'Milk, eggs, bread')
await page.click('button[type="submit"]')
// Verify task appears
await expect(page.locator('text=Buy groceries')).toBeVisible()
})
test('can complete a task', async ({ page }) => {
await page.goto('/tasks')
// Find and complete task
const task = page.locator('.task', { hasText: 'Buy groceries' })
await task.locator('.toggle').click()
// Verify completed state
await expect(task).toHaveClass(/completed/)
})
test('can filter tasks', async ({ page }) => {
await page.goto('/tasks')
// Filter to pending only
await page.click('text=Pending')
// Completed task should be hidden
await expect(page.locator('.task.completed')).toHaveCount(0)
})
}) E2E tests are slower but test the full stack. Use them for critical paths.
What to Test
Always test:
- Load function authorization and error handling
- Form validation logic
- Critical user flows (signup, checkout, etc.)
Usually test:
- Complex derived state logic
- Component interaction behavior
- Edge cases in business logic
Rarely test:
- Simple prop passing
- CSS styling
- Third-party library behavior
Test Structure
src/
├── lib/
│ ├── components/
│ │ ├── TaskCard.svelte
│ │ └── TaskCard.test.ts # Component tests
│ └── server/
│ ├── db.ts
│ └── db.test.ts # Utility tests
├── routes/
│ └── tasks/
│ ├── +page.server.ts
│ └── +page.server.test.ts # Load function tests
└── tests/
└── tasks.spec.ts # E2E tests Co-locate unit and component tests. Keep E2E tests in a separate folder.
Running Tests
# Unit and component tests
npm run test
# E2E tests
npm run test:e2e
# Watch mode for development
npm run test -- --watch Tests are documentation. They show how code is meant to be used. When tests are easy to write, it usually means the code is well-designed.