Testing
VentureKit apps test at two layers:
- Unit — invoke handlers in-process with no AWS, using the helpers from
@venturekit/runtime/testing(mockEvent,mockContext,createTestHandler). Fast, no Docker. See the runtime package. - Integration & end-to-end — boot a real local stack (
vk migrate+vk dev→ Postgres / MinIO / cognito-local) and drive it through the API or a real browser. This is what@venturekit/testingexists for, and what this guide covers.
Scaffold the setup
Section titled “Scaffold the setup”The fastest way to start is the scaffolder, which writes a Playwright config,
a global setup, a reference spec, and patches package.json + .gitignore:
vk generate e2eIt reads vk.config.ts to find the database vk dev will serve and whether the project declares an auth intent, then creates:
playwright.config.ts # vkWebServers(): create DB → migrate → `vk dev --no-crons`, gated on /_dev/healthe2e/ health.spec.ts # a passing health check + skipped examples global-setup.ts # (auth projects) wipes stale .auth/ cookie jars roles.setup.ts # (auth projects) establishRoles(): create + log in each role once…and adds test:e2e / test:e2e:ui scripts, @playwright/test and
@venturekit/testing (plus pg when there is a database to create) to
devDependencies, and Playwright’s output dirs to .gitignore.
| Option | Description | Default |
|---|---|---|
-p, --port <port> | Port the test API runs on (also honoured via E2E_API_PORT) | 4001 |
-s, --stage <stage> | Stage the suite runs against — its own <database>_<stage> database | test |
--filter <pkg> | pnpm workspace filter for the package holding vk.config.ts (monorepos) | none |
-f, --force | Overwrite existing files | false |
Then install and run:
pnpm installnpx playwright install # browsers — only needed for UI (page) testspnpm test:e2eThe generated Playwright config
Section titled “The generated Playwright config”Playwright owns the vk dev process via its webServer block. The scaffold
delegates that block to vkWebServers() from @venturekit/testing/playwright,
which encodes three ordering rules that otherwise fail far from their cause:
create the stage database first (nothing in the CLI does, and globalSetup
runs too late), migrate the database vk dev will actually serve (it
derives <database>_<stage> from vk.config.ts and ignores DATABASE_URL),
and keep crons off (--no-crons) so schedules don’t write to the rows your
specs assert on.
import { defineConfig } from '@playwright/test';import { storageStatePath, vkWebServers } from '@venturekit/testing/playwright';
const STAGE = 'test';const API_PORT = Number(process.env.E2E_API_PORT ?? 4001);const API_URL = `http://127.0.0.1:${API_PORT}`;
export default defineConfig({ testDir: './e2e', globalSetup: './e2e/global-setup.ts', // auth projects only timeout: 30_000, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list', use: { baseURL: API_URL, trace: 'on-first-retry' }, projects: [ // Logs every role in once and saves its cookie jar. { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'api', dependencies: ['setup'], testIgnore: /.*\.setup\.ts/, use: { baseURL: API_URL, storageState: storageStatePath('admin') }, }, ], // create the stage database, migrate + seed it, then boot `vk dev` with crons off webServer: vkWebServers({ stage: STAGE, database: 'app', // from vk.config.ts — must match what `vk dev` derives api: { cwd: '.', port: API_PORT }, // or { filter: '@acme/api', port } in a pnpm workspace // Add your UI here for browser specs, then point `use.baseURL` at it: // web: { name: 'Admin', command: 'pnpm --filter @acme/admin exec next dev -p 3100', url: 'http://127.0.0.1:3100' }, }),});Roles, cookie jars and per-role fixtures
Section titled “Roles, cookie jars and per-role fixtures”For projects with an auth intent the scaffold adds a setup project that
creates each cognito-local user, logs it in once, and writes .auth/<role>.json;
specs then adopt a role through storageState instead of logging in.
import { test as setup } from '@playwright/test';import { establishRoles } from '@venturekit/testing/playwright';
setup('establish roles', async () => { await establishRoles({ baseUrl: API_URL, // `provision` (default true) creates each user on cognito-local first — idempotent, // an existing user just gets its password reset. Add cognito attributes a role needs: roles: { admin: { email: 'admin@example.com', password: 'Passw0rd!', attributes: { tenantId: 'dev' } }, viewer: { email: 'viewer@example.com', password: 'Passw0rd!' }, }, });});
// e2e/global-setup.ts — stale jars point at users the fresh database no longer hasimport { wipeAuthDir } from '@venturekit/testing/playwright';export default async function globalSetup() { await wipeAuthDir(); }
// tests/api/fixtures.ts — authz specs need several roles in one fileimport { expect } from '@playwright/test';import { createVkTest } from '@venturekit/testing/playwright';export const test = createVkTest();test('viewers cannot publish', async ({ asRole }) => { const viewer = await asRole('viewer'); expect((await viewer.post('/content')).status()).toBe(403);});Manual setup: readiness, auth, and DB reset
Section titled “Manual setup: readiness, auth, and DB reset”If you own the vk dev process yourself instead of using vkWebServers(), a
global-setup.ts can confirm the server is really a vk dev server, seed a
login and reset the database:
import { createTestUser, truncateAllTables, waitForReady } from '@venturekit/testing';
const API_URL = `http://localhost:${Number(process.env.E2E_API_PORT ?? 4001)}`;
export default async function globalSetup() { await waitForReady({ baseUrl: API_URL });
// Reset the DB between runs (needs @venturekit/data + DATABASE_URL / DB_*): await truncateAllTables();
// Seed a known login if your API declares an `auth` intent: await createTestUser({ baseUrl: API_URL, email: 'admin@example.com', password: 'Passw0rd!', attributes: { tenantId: 'global' }, });}waitForReady()polls/_dev/health— the same probevktooling uses.createTestUser()provisions a cognito-local user with a permanent password over thevk devadmin API. No AWS SDK required.truncateAllTables()clears app tables but preserves the__vk_migrations/__vk_seedsbookkeeping, so migrations don’t re-run.
Write a UI spec
Section titled “Write a UI spec”With the login seeded, specs drive the real UI — the most faithful E2E:
import { test, expect } from '@playwright/test';
test('logs in and lands on the dashboard', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('admin@example.com'); await page.getByLabel('Password').fill('Passw0rd!'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page).toHaveURL(/\/(inbox|dashboard)/);});The scaffolded health.spec.ts uses Playwright’s request fixture (no
browser), so it runs without npx playwright install:
import { test, expect } from '@playwright/test';
const API_URL = `http://localhost:${Number(process.env.E2E_API_PORT ?? 4001)}`;
test('the API dev server is up', async ({ request }) => { const res = await request.get(`${API_URL}/_dev/health`); expect(res.ok()).toBeTruthy(); expect((await res.json()).vk).toBe('dev-server');});API-level integration tests (vitest)
Section titled “API-level integration tests (vitest)”When you don’t need a browser, own the stack yourself, mint a token, and hit
the API directly. createApiClient() unwraps the { data } envelope and
throws a typed ApiError on non-2xx:
import { afterAll, beforeAll, expect, test } from 'vitest';import { startTestStack, loginAs, createApiClient, type TestStack } from '@venturekit/testing';
let stack: TestStack;
beforeAll(async () => { stack = await startTestStack({ port: 4100, seed: true });}, 180_000);
afterAll(async () => { await stack?.stop();});
test('GET /tenant returns the current tenant', async () => { const { idToken } = await loginAs({ baseUrl: stack.baseUrl, email: 'admin@example.com', password: 'Passw0rd!', }); const api = createApiClient({ baseUrl: stack.baseUrl, token: idToken }); const res = await api.get<{ slug: string }>('/tenant'); expect(res.status).toBe(200); expect(res.data.slug).toBeDefined();});loginAs() needs the optional @aws-sdk/client-cognito-identity-provider
peer. See the @venturekit/testing reference for the full
API.
Run end-to-end tests in CI
Section titled “Run end-to-end tests in CI”Generate a GitHub Actions workflow that runs the suite on every pull request:
vk gha e2eThis writes .github/workflows/e2e.yml. It needs no AWS credentials — the
GitHub-hosted ubuntu-latest runner ships with Docker, which vk dev uses to
start Postgres / MinIO / cognito-local.
| Option | Description | Default |
|---|---|---|
-p, --path <file> | Output path | .github/workflows/e2e.yml |
--branch <branch...> | Branches that also trigger E2E on push | main |
--force | Overwrite an existing workflow file | false |
The generated workflow installs dependencies and Playwright browsers, then
runs pnpm test:e2e — the Playwright webServer block migrates the test DB,
boots vk dev, and waits on /_dev/health before the specs run:
name: e2e
on: pull_request: push: branches: ["main"] workflow_dispatch:
concurrency: group: e2e-${{ github.ref }} cancel-in-progress: true
jobs: e2e: name: end-to-end runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: version: 9 - uses: actions/setup-node@v4 with: node-version: '20' cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run build --if-present - name: Install Playwright browsers run: pnpm exec playwright install --with-deps chromium - name: Run end-to-end tests run: pnpm run test:e2e env: CI: 'true' - name: Upload Playwright report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 7Related
Section titled “Related”- @venturekit/testing — full harness API reference
- CLI: generate —
vk generate e2e - CLI: dev — the local stack these tests drive
- CLI: migrate — how the test DB is prepared
- Authentication — the Cognito model the auth helpers seed
- Deployment — the deploy workflow
vk gha e2ecomplements