I run 874 automated tests across three SvelteKit starter kits — the same multi-tenant service layer (orgs → members → invites → RBAC → billing → audit) exercised against three different data backends: SQLite/D1, Supabase, and vendor-neutral Postgres. The split is 298 + 314 + 262 per kit, and each suite sits at ≥95% line, branch, function, and statement coverage, enforced by the Vitest config.
Here's the testing strategy that caught real bugs — and the one that wasted my time.
Tier 1: unit tests (fast, no database)
Test service functions in isolation with the database mocked. These run in a couple of seconds.
// tests/rbac.test.ts
import { describe, it, expect } from 'vitest';
import { mayActOn, MATRIX } from '../src/lib/server/rbac';
describe('RBAC hierarchy', () => {
it('owner can act on admin', () => {
expect(mayActOn('owner', 'admin', 'members.role.set')).toBe(true);
});
it('admin cannot act on owner', () => {
expect(mayActOn('admin', 'owner', 'members.role.set')).toBe(false);
});
it('member cannot invite', () => {
expect(MATRIX.member).not.toContain('members.invite');
});
});
What these catch: logic bugs in permission checks, role hierarchy violations, and business-rule edge cases.
What these miss: schema mismatches, constraint violations, RLS policies, migration bugs — everything that lives in the gap between your code and the database.
Tier 2: integration tests (real database)
Spin up a real test database and run actual queries. In the kits, a Docker Compose file starts Postgres on a dedicated test port, and each test wipes the tables first.
// tests/orgs-members.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createOrg } from '../src/lib/server/services/orgs';
let db: any;
beforeEach(async () => {
// Truncate all tables between tests
await db.execute(sql`TRUNCATE TABLE audit_log, invites, memberships, organizations CASCADE`);
});
describe('org creation', () => {
it('creates org with creator as owner', async () => {
const org = await createOrg(db, { name: 'Test Org', slug: 'test-org', userId: 'user-1' });
expect(org.slug).toBe('test-org');
const members = await db.select().from(memberships).where(eq(memberships.orgId, org.id));
expect(members[0].role).toBe('owner');
expect(members[0].userId).toBe('user-1');
});
it('rejects duplicate slugs', async () => {
await createOrg(db, { name: 'Org 1', slug: 'dupe', userId: 'user-1' });
await expect(
createOrg(db, { name: 'Org 2', slug: 'dupe', userId: 'user-2' })
).rejects.toThrow();
});
});
What these catch: schema drift, foreign-key and unique-constraint violations, migration ordering bugs, and type mismatches between the code and the actual database. These are the bugs that unit tests simply can't see.
What these miss: HTTP-level issues — hooks misconfiguration, auth-flow wiring, redirect logic.
Tier 3: HTTP-level tests (through SvelteKit)
Push real Request objects through the SvelteKit app itself. This exercises hooks.server.ts, auth, and routing — the whole pipeline, no browser needed.
// tests/http.test.ts
import { describe, it, expect } from 'vitest';
import { app } from '../src/app';
describe('POST /app/org/create', () => {
it('requires authentication', async () => {
const response = await app.request(new Request('http://localhost/app/org/create', {
method: 'POST',
body: new URLSearchParams({ name: 'Test' }),
}));
expect(response.status).toBe(303); // redirect to login
});
it('creates an org for an authenticated user', async () => {
const response = await app.request(new Request('http://localhost/app/org/create', {
method: 'POST',
body: new URLSearchParams({ name: 'My Org', slug: 'my-org' }),
headers: { cookie: 'session=valid-token-here' },
}));
expect(response.status).toBe(303);
expect(response.headers.get('location')).toContain('/app');
});
});
What these catch: hook misconfigurations, cookie and redirect handling, CSRF wiring — the glue that unit and integration tests never touch.
One config detail that cost me an afternoon: for HTTP tests inside Vitest you need the SvelteKit plugin and an inline dep so $app/$env aliases resolve:
// vitest.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['tests/**/*.test.ts'],
server: {
deps: { inline: ['@sveltejs/kit'] }, // resolve $app / $env aliases
},
fileParallelism: false, // tier 2 tests share one database
testTimeout: 10_000,
},
});
The mistake I made early on
I mocked the database for everything. The suite was fast — hundreds of unit tests finished in seconds — but in my experience it caught maybe 40% of the bugs that actually reached production. The other 60% were:
- Schema drift (code says
role TEXT, the database saysVARCHAR(20)) - Migration-ordering bugs (the column doesn't exist yet)
- RLS policies silently rejecting legitimate queries
- Constraint violations on edge cases (unique indexes, FK cascades)
Adding a real-database integration tier moved almost all of that earlier in the pipeline. The suite got slower — the full run takes on the order of a minute — and production incidents dropped to near zero. Those catch-rate figures are estimates from my own projects, not law; the direction is what matters.
What I'd recommend
| Situation | Tier to run | Speed | What it catches (est.) |
|---|---|---|---|
| Quick feedback during dev | Tier 1 (unit) | seconds | ~40% of production bugs |
| CI/CD gate | Tier 1 + 2 | ~under a minute | ~85% of production bugs |
| Full verification | All 3 tiers | ~a minute | ~95% of production bugs |
The key insight: all three tiers are necessary. Unit tests alone miss too much; integration tests alone are too slow for rapid iteration; HTTP tests alone don't catch business-logic bugs. And cap it all with a coverage gate — ≥95% on line, branch, function, and statement — so the suite can't quietly shrink while features grow.
The patterns above are what the VerdantStack kits actually run: Multi-tenant SvelteKit Starter (SQLite/D1, v0.2.8, 298 tests), SvelteKit + Supabase Starter (v0.2.6, 314 tests), and SvelteKit + Postgres Starter (vendor-neutral Postgres, v0.1.6, 262 tests) — every suite ≥95% coverage on all four metrics, enforced in CI. Source code (public proof repos, source excluded by design): github.com/verdantstack.
Live demo of the Postgres kit: postgres-starter.verdantstack-site.pages.dev — running its 262 tests against a real Postgres database.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.