--- title: Testing sort: 120 section-id: guides keywords: testing, unit tests, integration tests, E2E, Playwright, Vitest, testing strategy description: Testing Velox applications with unit tests, integration tests, and end-to-end tests using Playwright language: en --- # Testing Velox integrates with Vitest for unit and integration tests, and Playwright for end-to-end tests. The `@velox/test` package provides additional test utilities tailored for Velox's server blocks and API routes. ## Setup ```bash npm install -D vitest @velox/test @playwright/test npx playwright install ``` Add test scripts to `package.json`: ```json { "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:e2e": "playwright test", "test:coverage": "vitest --coverage" } } ``` Configure Vitest: ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; import { veloxTestPlugin } from '@velox/test'; export default defineConfig({ plugins: [veloxTestPlugin()], test: { environment: 'node', globals: true, setupFiles: ['./tests/setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'lcov'], }, }, }); ``` ## Unit Tests ### Testing Utility Functions ```typescript // lib/formatters.ts export function formatCurrency(amount: number, currency = 'USD'): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount); } ``` ```typescript // tests/unit/formatters.test.ts import { describe, it, expect } from 'vitest'; import { formatCurrency } from '$lib/formatters'; describe('formatCurrency', () => { it('formats USD amounts', () => { expect(formatCurrency(1000)).toBe('$1,000.00'); expect(formatCurrency(9.99)).toBe('$9.99'); }); it('formats other currencies', () => { expect(formatCurrency(1000, 'EUR')).toBe('€1,000.00'); }); it('handles zero', () => { expect(formatCurrency(0)).toBe('$0.00'); }); }); ``` ### Testing Components ```tsx // tests/unit/Counter.test.tsx import { describe, it, expect } from 'vitest'; import { render, fireEvent } from '@velox/test'; import Counter from '$components/Counter'; describe('Counter', () => { it('renders with initial value', () => { const { getByText } = render(); expect(getByText('5')).toBeTruthy(); }); it('increments on button click', async () => { const { getByText, getByRole } = render(); await fireEvent.click(getByRole('button', { name: /increment/i })); expect(getByText('1')).toBeTruthy(); }); it('decrements below initial value', async () => { const { getByText, getByRole } = render(); await fireEvent.click(getByRole('button', { name: /decrement/i })); expect(getByText('2')).toBeTruthy(); }); }); ``` ## Integration Tests ### Testing API Routes The `createTestServer` utility starts a real Velox server for integration testing: ```typescript // tests/integration/api/users.test.ts import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { createTestServer } from '@velox/test'; import { db } from '$lib/db'; let server: Awaited>; beforeAll(async () => { server = await createTestServer({ seed: true }); }); afterAll(async () => { await server.stop(); await db.$disconnect(); }); describe('GET /api/users', () => { it('returns all users', async () => { const res = await server.inject({ method: 'GET', url: '/api/users', headers: { Authorization: `Bearer ${server.getAdminToken()}` }, }); expect(res.status).toBe(200); const body = await res.json(); expect(Array.isArray(body)).toBe(true); expect(body.length).toBeGreaterThan(0); }); it('returns 401 without auth', async () => { const res = await server.inject({ method: 'GET', url: '/api/users' }); expect(res.status).toBe(401); }); }); describe('POST /api/users', () => { it('creates a new user', async () => { const res = await server.inject({ method: 'POST', url: '/api/users', body: { email: 'new@example.com', name: 'New User' }, headers: { Authorization: `Bearer ${server.getAdminToken()}` }, }); expect(res.status).toBe(201); const user = await res.json(); expect(user.email).toBe('new@example.com'); expect(user.id).toBeDefined(); }); it('rejects duplicate email', async () => { await server.inject({ method: 'POST', url: '/api/users', body: { email: 'dup@example.com', name: 'First' }, headers: { Authorization: `Bearer ${server.getAdminToken()}` }, }); const res = await server.inject({ method: 'POST', url: '/api/users', body: { email: 'dup@example.com', name: 'Second' }, headers: { Authorization: `Bearer ${server.getAdminToken()}` }, }); expect(res.status).toBe(409); }); }); ``` ### Testing Database Operations ```typescript // tests/integration/db/posts.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { db } from '$lib/db'; import { createTestUser, createTestPost } from '../factories'; beforeEach(async () => { await db.$executeRaw`TRUNCATE posts, users RESTART IDENTITY CASCADE`; }); describe('Post queries', () => { it('finds published posts only', async () => { const user = await createTestUser(); await createTestPost({ authorId: user.id, published: true }); await createTestPost({ authorId: user.id, published: false }); const posts = await db.post.findMany({ where: { published: true } }); expect(posts).toHaveLength(1); }); }); ``` ## End-to-End Tests with Playwright ```typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', use: { baseURL: 'http://localhost:3700', screenshot: 'only-on-failure', video: 'retain-on-failure', }, webServer: { command: 'npm run dev', url: 'http://localhost:3700', reuseExistingServer: !process.env.CI, }, }); ``` ```typescript // tests/e2e/auth.spec.ts import { test, expect } from '@playwright/test'; test.describe('Authentication', () => { test('user can log in', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Password').fill('correct-password'); await page.getByRole('button', { name: 'Log in' }).click(); await page.waitForURL('/dashboard'); await expect(page.getByText('Welcome back')).toBeVisible(); }); test('shows error for invalid credentials', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Password').fill('wrong-password'); await page.getByRole('button', { name: 'Log in' }).click(); await expect(page.getByRole('alert')).toContainText('Invalid credentials'); await expect(page).toHaveURL('/login'); }); }); ``` ## Test Factories Use factories to generate test data consistently: ```typescript // tests/factories.ts import { db } from '$lib/db'; import { hashPassword } from '$lib/crypto'; export async function createTestUser(overrides = {}) { return db.user.create({ data: { email: `user-${Date.now()}@example.com`, name: 'Test User', passwordHash: await hashPassword('test-password'), ...overrides, }, }); } export async function createTestPost(overrides = {}) { return db.post.create({ data: { title: 'Test Post', slug: `test-post-${Date.now()}`, content: 'Test content', published: true, ...overrides, }, }); } ``` ## CI Configuration ```yaml # .github/workflows/test.yml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: test options: >- --health-cmd pg_isready --health-interval 10s steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '22' } - run: npm ci - run: npx prisma migrate deploy env: DATABASE_URL: postgresql://postgres:postgres@localhost/test - run: npm test - run: npx playwright install --with-deps - run: npm run test:e2e ```