mdcms/sample-sites/velox-docs/pages/guide-testing.md
2026-05-18 14:30:49 +07:00

8.3 KiB

title sort section-id keywords description language
Testing 120 guides testing, unit tests, integration tests, E2E, Playwright, Vitest, testing strategy Testing Velox applications with unit tests, integration tests, and end-to-end tests using Playwright 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

npm install -D vitest @velox/test @playwright/test
npx playwright install

Add test scripts to package.json:

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:e2e": "playwright test",
    "test:coverage": "vitest --coverage"
  }
}

Configure Vitest:

// 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

// lib/formatters.ts
export function formatCurrency(amount: number, currency = 'USD'): string {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}
// 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

// 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(<Counter initialValue={5} />);
    expect(getByText('5')).toBeTruthy();
  });

  it('increments on button click', async () => {
    const { getByText, getByRole } = render(<Counter initialValue={0} />);
    await fireEvent.click(getByRole('button', { name: /increment/i }));
    expect(getByText('1')).toBeTruthy();
  });

  it('decrements below initial value', async () => {
    const { getByText, getByRole } = render(<Counter initialValue={3} />);
    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:

// 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<ReturnType<typeof createTestServer>>;

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

// 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

// 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,
  },
});
// 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:

// 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

# .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