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

playwright.config.ts # boots `vk dev`, gates on /_dev/health
e2e/
global-setup.ts # waitForReady + (commented) auth seeding / DB reset
health.spec.ts # a passing health check + skipped examples

…and adds test:e2e / test:e2e:ui scripts plus @playwright/test and @venturekit/testing to devDependencies.

OptionDescriptionDefault
-p, --port <port>Port the test API runs on (also honoured via E2E_API_PORT)4001
-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, and waits on the /_dev/health readiness probe before any spec runs:

playwright.config.ts
import { defineConfig } from '@playwright/test';
// The API runs on an isolated test stage/port so E2E never clobbers
// your `vk dev` data. Override with E2E_API_PORT.
const API_PORT = Number(process.env.E2E_API_PORT ?? 4001);
const API_URL = `http://localhost:${API_PORT}`;
export default defineConfig({
testDir: './e2e',
globalSetup: './e2e/global-setup.ts',
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',
},
webServer: {
// Migrate the isolated `test` DB, then boot the API. `vk dev` does
// not auto-migrate, so the migrate step keeps local + CI consistent.
command: `vk migrate --env test && vk dev --stage test --port ${API_PORT} --no-watch`,
url: `${API_URL}/_dev/health`,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});

Global setup: readiness, auth, and DB reset

Section titled “Global setup: readiness, auth, and DB reset”

global-setup.ts runs once before the suite. It confirms the server is really a vk dev server, and is where you seed a login or 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