Skip to content

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/testing exists for, and what this guide covers.

The fastest way to start is the scaffolder, which writes a Playwright config, a global setup, a reference spec, and patches package.json + .gitignore:

Terminal window
vk generate e2e

It 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/health
e2e/
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.

OptionDescriptionDefault
-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> databasetest
--filter <pkg>pnpm workspace filter for the package holding vk.config.ts (monorepos)none
-f, --forceOverwrite existing filesfalse

Then install and run:

Terminal window
pnpm install
npx playwright install # browsers — only needed for UI (page) tests
pnpm test:e2e

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.

playwright.config.ts
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' },
}),
});

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.

e2e/roles.setup.ts
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 has
import { wipeAuthDir } from '@venturekit/testing/playwright';
export default async function globalSetup() { await wipeAuthDir(); }
// tests/api/fixtures.ts — authz specs need several roles in one file
import { 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:

e2e/global-setup.ts
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 probe vk tooling uses.
  • createTestUser() provisions a cognito-local user with a permanent password over the vk dev admin API. No AWS SDK required.
  • truncateAllTables() clears app tables but preserves the __vk_migrations / __vk_seeds bookkeeping, so migrations don’t re-run.

With the login seeded, specs drive the real UI — the most faithful E2E:

e2e/login.spec.ts
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');
});

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.

Generate a GitHub Actions workflow that runs the suite on every pull request:

Terminal window
vk gha e2e

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

OptionDescriptionDefault
-p, --path <file>Output path.github/workflows/e2e.yml
--branch <branch...>Branches that also trigger E2E on pushmain
--forceOverwrite an existing workflow filefalse

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