Testing

Svelar provides a Laravel-inspired testing module built on top of Vitest (unit/feature) and Playwright (end-to-end). New projects scaffolded with npx svelar new come with everything pre-configured.

import { useSvelarTest, assertDatabaseHas, Factory, actingAs, createRequestEvent } from '@beeblock/svelar/testing';

Quick Start

Scaffolded projects include test infrastructure out of the box:

npm test              # Run unit + feature tests
npm run test:watch    # Watch mode
npm run test:e2e      # Run Playwright e2e tests
npm run test:coverage # Coverage report

Generate test files and factories with the CLI:

npx svelar make:test UserService --unit     # tests/unit/UserService.test.ts
npx svelar make:test Auth --feature         # tests/feature/Auth.test.ts
npx svelar make:test Login --e2e            # tests/e2e/Login.spec.ts
npx svelar make:factory User --model User   # src/lib/factories/UserFactory.ts

Project Structure

my-app/
  vitest.config.ts          # Vitest config with aliases
  playwright.config.ts      # Playwright config
  tests/
    unit/                   # Unit tests (*.test.ts)
      example.test.ts
    feature/                # Feature tests with DB (*.test.ts)
      auth.test.ts
    e2e/                    # Playwright tests (*.spec.ts)
  src/lib/
    factories/              # Model factories
      UserFactory.ts

useSvelarTest()

The useSvelarTest() composable wires up the test environment automatically inside a describe() block. It configures an in-memory SQLite database and resets factory sequences between tests.

import { describe, it, expect } from 'vitest';
import { useSvelarTest, assertDatabaseHas } from '@beeblock/svelar/testing';
import UserFactory from '$lib/factories/UserFactory';

describe('UserService', () => {
  useSvelarTest({ refreshDatabase: true });

  it('creates a user', async () => {
    const user = await UserFactory.create({ name: 'Alice' });
    await assertDatabaseHas('users', { name: 'Alice' });
  });
});

Options

Option Type Default Description
refreshDatabase boolean false Drop all tables and re-run migrations before each test
connectionName string undefined Database connection name (uses default if omitted)

Factories

Factories generate model instances with sensible defaults. Extend the Factory<T> base class and implement model() and definition().

import { Factory } from '@beeblock/svelar/testing';
import { User } from '$lib/modules/auth/User';

export class UserFactory extends Factory<User> {
  model() {
    return User;
  }

  definition() {
    return {
      name: `User ${this.sequence}`,
      email: `user${this.sequence}@test.com`,
      password_hash: 'hashed',
      role: 'user',
    };
  }
}

// Export a singleton for convenience
export default new UserFactory();

Factory Methods

// Create and persist to database
const user = await UserFactory.create({ name: 'Alice' });

// Create multiple
const users = await UserFactory.createMany(5, { role: 'admin' });

// Make without persisting (in-memory only)
const user = UserFactory.make({ name: 'Bob' });

// Make multiple without persisting
const users = UserFactory.makeMany(3);

Generating Factories

npx svelar make:factory User --model User
npx svelar make:factory Post --model Post

The --model flag specifies which model class to import. The command auto-detects DDD vs flat project structure for the import path.

Database Assertions

Assert against your database state in tests. These throw descriptive errors on failure.

import { assertDatabaseHas, assertDatabaseMissing, assertDatabaseCount } from '@beeblock/svelar/testing';

// Assert a matching row exists
await assertDatabaseHas('users', { email: 'alice@test.com' });

// Assert no matching row exists
await assertDatabaseMissing('users', { email: 'deleted@test.com' });

// Assert exact row count
await assertDatabaseCount('users', 5);

// Assert count with conditions
await assertDatabaseCount('users', 2, { role: 'admin' });

refreshDatabase()

Drops all tables and re-runs migrations from src/lib/database/migrations/. Called automatically when useSvelarTest({ refreshDatabase: true }) is set, but can also be used standalone:

import { refreshDatabase } from '@beeblock/svelar/testing';

beforeEach(async () => {
  await refreshDatabase();
});

actingAs()

Simulate an authenticated user by setting event.locals.user, just like AuthenticateMiddleware does in production.

import { actingAs, createRequestEvent } from '@beeblock/svelar/testing';
import UserFactory from '$lib/factories/UserFactory';

it('returns user profile', async () => {
  const user = await UserFactory.create();
  const event = actingAs(user, createRequestEvent({
    method: 'GET',
    url: '/api/me',
  }));

  const response = await controller.handle('show')(event);
  expect(response.status).toBe(200);
});

actingAs() accepts either an existing RequestEvent or options to create one:

// With existing event
const event = createRequestEvent({ method: 'POST', url: '/api/posts' });
actingAs(user, event);

// With options (creates a new event)
const event = actingAs(user, { method: 'GET', url: '/api/me' });

// With no options (creates a default GET / event)
const event = actingAs(user);

createRequestEvent()

Build a mock SvelteKit RequestEvent for testing server-side handlers without a running server.

import { createRequestEvent } from '@beeblock/svelar/testing';

const event = createRequestEvent({
  method: 'POST',
  url: '/api/posts',
  headers: { 'Content-Type': 'application/json' },
  body: { title: 'Hello', content: 'World' },
  params: { id: '1' },
  locals: { user: someUser },
  cookies: { session_id: 'abc123' },
});

Options

Option Type Default Description
method string 'GET' HTTP method
url string 'http://localhost:5173/' Request URL
headers Record<string, string> {} Request headers
body any undefined Request body (auto-serialized to JSON)
params Record<string, string> {} Route parameters
locals Record<string, any> {} SvelteKit locals
cookies Record<string, string> {} Request cookies

Testing Controllers

Combine factories, actingAs(), and createRequestEvent() to test your controllers:

import { describe, it, expect } from 'vitest';
import { useSvelarTest, actingAs, createRequestEvent, assertDatabaseHas } from '@beeblock/svelar/testing';
import UserFactory from '$lib/factories/UserFactory';
import { PostController } from '$lib/modules/posts/PostController';

describe('PostController', () => {
  useSvelarTest({ refreshDatabase: true });
  const controller = new PostController();

  it('creates a post', async () => {
    const user = await UserFactory.create();
    const event = actingAs(user, createRequestEvent({
      method: 'POST',
      url: '/api/posts',
      body: { title: 'My Post', content: 'Hello world' },
    }));

    const response = await controller.handle('store')(event);
    expect(response.status).toBe(201);
    await assertDatabaseHas('posts', { title: 'My Post', user_id: user.id });
  });

  it('requires authentication', async () => {
    const event = createRequestEvent({
      method: 'POST',
      url: '/api/posts',
      body: { title: 'Unauthorized' },
    });

    const response = await controller.handle('store')(event);
    expect(response.status).toBe(401);
  });
});

E2E Tests with Playwright

E2E tests live in tests/e2e/ and use Playwright. The scaffolded playwright.config.ts starts your dev server automatically.

import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('can log in', async ({ page }) => {
    await page.goto('/login');
    await page.fill('input[name="email"]', 'admin@svelar.dev');
    await page.fill('input[name="password"]', 'admin123');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL('/dashboard');
  });

  test('rejects invalid credentials', async ({ page }) => {
    await page.goto('/login');
    await page.fill('input[name="email"]', 'wrong@test.com');
    await page.fill('input[name="password"]', 'wrong');
    await page.click('button[type="submit"]');
    await expect(page.locator('.error')).toBeVisible();
  });
});

Generate e2e tests:

npx svelar make:test Login --e2e       # tests/e2e/Login.spec.ts
npx svelar make:test Dashboard --e2e   # tests/e2e/Dashboard.spec.ts

Configuration

vitest.config.ts

The scaffolded config includes aliases matching your vite.config.ts so $lib and @beeblock/svelar/* imports work in tests:

import { defineConfig } from 'vitest/config';
import { createRequire } from 'module';
import { dirname, resolve } from 'path';

const require_ = createRequire(import.meta.url);
const svelarRoot = dirname(require_.resolve('@beeblock/svelar/package.json'));

export default defineConfig({
  test: {
    globals: true,
    include: ['tests/unit/**/*.test.ts', 'tests/feature/**/*.test.ts'],
    exclude: ['tests/e2e/**'],
    alias: {
      '$lib': resolve('./src/lib'),
      '@beeblock/svelar/testing': resolve(svelarRoot, 'dist/testing/index.js'),
      // ... other svelar module aliases
    },
  },
});

playwright.config.ts

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: 'tests/e2e',
  testMatch: '**/*.spec.ts',
  fullyParallel: true,
  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
  },
});

CLI Reference

Command Description
make:test Name --unit Unit test in tests/unit/Name.test.ts (default)
make:test Name --feature Feature test in tests/feature/Name.test.ts
make:test Name --e2e Playwright test in tests/e2e/Name.spec.ts
make:factory Name --model Model Factory in src/lib/factories/NameFactory.ts

API Reference

Export Description
useSvelarTest(options?) Wire up test environment in a describe() block
refreshDatabase(connectionName?) Drop all tables, re-run migrations
Factory<T> Base class for model factories
assertDatabaseHas(table, conditions) Assert matching row exists
assertDatabaseMissing(table, conditions) Assert no matching row exists
assertDatabaseCount(table, expected, conditions?) Assert exact row count
actingAs(user, event?) Simulate authenticated user
createRequestEvent(options?) Build mock SvelteKit RequestEvent
Svelar © 2026 · MIT License