neon-testing
v2.5.1
Published
A Vitest utility for seamless integration tests with Neon Postgres
Maintainers
Readme
Neon Testing
A Vitest utility for seamless integration tests with Neon Postgres.
Each test file runs against its own isolated PostgreSQL database (Neon branch), ensuring clean, parallel, and reproducible testing of code that interacts with a database. Because it uses a real, isolated clone of your production database, you can test code logic that depends on database features, such as transaction rollbacks, unique constraints, and more.
Testing against a clone of your production database lets you verify functionality that mocks cannot.
Table of Contents
- Features
- How it works
- Quick start
- Drivers
- Configuration
- Continuous integration
- API Reference
- Contributing
- Author
Features
- 🔄 Isolated test environments - Each test file runs against its own Postgres database with your actual schema and constraints
- 🧹 Automatic cleanup - Neon test branches are created and destroyed automatically
- 🐛 Debug friendly - Option to preserve test branches for debugging failed tests
- 🛡️ TypeScript native - With JavaScript support
- 🎯 ESM only - No CommonJS support
How it works
- Branch creation: Before tests run, a new Neon branch is created with a unique name
- Environment setup:
DATABASE_URLis set to point to your test branch - Test execution: Your tests run against the isolated database
- Cleanup: After tests complete, the branch is automatically deleted
Test isolation
Tests in the same file share a single database instance (Neon branch). This means test files are fully isolated from each other, but individual tests within a file are intentionally not isolated. This works because Vitest runs test files in parallel, while tests within each file run sequentially.
If you prefer individual tests to be isolated, you can reset the database in a beforeEach lifecycle hook.
Automatic cleanup
Test branches are automatically deleted after your tests complete. As a safety net, branches also expire after 10 minutes by default to handle interrupted or failed test runs. This dual approach minimizes costs while protecting against edge cases like crashed processes or CI failures. Both behaviors can be customized through the deleteBranch and expiresIn options.
Quick start
Prerequisites
- A Neon project with a database
- A Neon API key for programmatic access
Install
bun add -d neon-testing vitestMinimal example
// minimal.test.ts
import { expect, test } from "vitest";
import { makeNeonTesting } from "neon-testing";
import { Pool } from "@neondatabase/serverless";
// Enable Neon test branch for this test file
makeNeonTesting({
apiKey: process.env.NEON_API_KEY!,
projectId: process.env.NEON_PROJECT_ID!,
// Recommended for Neon WebSocket drivers to automatically close connections
autoCloseWebSockets: true,
})();
test("database operations", async () => {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await pool.query(`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`);
await pool.query(`INSERT INTO users (name) VALUES ('Ellen Ripley')`);
const users = await pool.query(`SELECT * FROM users`);
expect(users.rows).toStrictEqual([{ id: 1, name: "Ellen Ripley" }]);
});Source: examples/minimal.test.ts
Recommended usage
1. Plugin setup
First, add the Vite plugin to clear any existing DATABASE_URL environment variable before tests run, ensuring tests use isolated test databases.
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { neonTesting } from "neon-testing/vite";
export default defineConfig({
plugins: [neonTesting()],
});This plugin is recommended but not required. Without it, tests might accidentally use your existing DATABASE_URL (from .env files or environment variables) instead of the isolated test databases that Neon Testing creates. This can happen if you forget to call neonTesting() in a test file where database writes happen.
2. Configuration
Use the makeNeonTesting factory to generate a lifecycle function for your tests.
// neon-testing.ts
import { makeNeonTesting } from "neon-testing";
// Export a configured lifecycle function to use in test files
export const neonTesting = makeNeonTesting({
apiKey: process.env.NEON_API_KEY!,
projectId: process.env.NEON_PROJECT_ID!,
});Source: examples/neon-testing.ts
3. Enable database testing
Then call the exported test lifecycle function in the test files where you need database access.
// recommended.test.ts
import { expect, test } from "vitest";
import { neonTesting } from "./neon-testing";
import { Pool } from "@neondatabase/serverless";
// Enable Neon test branch for this test file
neonTesting({
// Recommended for Neon WebSocket drivers to automatically close connections
autoCloseWebSockets: true,
});
test("database operations", async () => {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await pool.query(`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`);
await pool.query(`INSERT INTO users (name) VALUES ('Ellen Ripley')`);
const users = await pool.query(`SELECT * FROM users`);
expect(users.rows).toStrictEqual([{ id: 1, name: "Ellen Ripley" }]);
});Source: examples/recommended.test.ts
Drivers
This library works with any database driver that supports Neon Postgres and Vitest. The examples below demonstrate connection management, transaction support, and test isolation patterns for some popular drivers.
IMPORTANT: For Neon WebSocket drivers, enable autoCloseWebSockets in your makeNeonTesting() or neonTesting() configuration. This automatically closes WebSocket connections when deleting test branches, preventing connection termination errors.
Examples
- Neon serverless WebSocket
- Neon serverless WebSocket + Drizzle
- Neon serverless HTTP
- Neon serverless HTTP + Drizzle
- node-postgres
- node-postgres + Drizzle
- Postgres.js
- Postgres.js + Drizzle
Configuration
You configure Neon Testing in two places:
- Base settings in
makeNeonTesting() - Optional overrides when calling the returned function (e.g.,
neonTesting())
Configure these in makeNeonTesting() and optionally override per test file when calling the returned function.
export interface NeonTestingOptions {
/**
* The Neon API key, this is used to create and teardown test branches (required)
*
* https://neon.com/docs/manage/api-keys#creating-api-keys
*/
apiKey: string;
/**
* The Neon project ID to operate on (required)
*
* https://console.neon.tech/app/projects
*/
projectId: string;
/**
* The parent branch ID for the new branch (default: undefined)
*
* If omitted or undefined, test branches will be created from the project's
* default branch.
*/
parentBranchId?: string;
/**
* Whether to create a schema-only branch (default: false)
*/
schemaOnly?: boolean;
/**
* The type of connection to create (default: "pooler")
*/
endpoint?: "pooler" | "direct";
/**
* Delete the test branch in afterAll (default: true)
*
* Disabling this will leave each test branch in the Neon project after the
* test suite runs
*/
deleteBranch?: boolean;
/**
* Automatically close Neon WebSocket connections opened during tests before
* deleting the branch (default: false)
*
* Suppresses the specific Neon WebSocket "Connection terminated unexpectedly"
* error that may surface when deleting a branch with open WebSocket
* connections
*/
autoCloseWebSockets?: boolean;
/**
* Time in seconds until the branch expires and is automatically deleted
* (default: 600 = 10 minutes)
*
* This provides automatic cleanup for dangling branches from interrupted or
* failed test runs. Set to `null` to disable automatic expiration.
*
* Must be a positive integer. Maximum 30 days (2,592,000 seconds).
*
* https://neon.com/docs/guides/branch-expiration
*/
expiresIn?: number | null;
/**
* The database role to connect as (default: project owner role)
*
* The role must exist in the parent branch. Roles are automatically
* copied to test branches when branching.
*/
roleName?: string;
/**
* The database to connect to (default: project default database)
*/
databaseName?: string;
}Base configuration
Configure the base settings in makeNeonTesting():
// neon-testing.ts
import { makeNeonTesting } from "neon-testing";
export const neonTesting = makeNeonTesting({
apiKey: process.env.NEON_API_KEY!,
projectId: process.env.NEON_PROJECT_ID!,
});Override configuration
Override the base configuration in specific test files when calling the function:
import { neonTesting } from "./neon-testing";
neonTesting({
parentBranchId: "br-staging-123",
});Role selection
You can connect as a specific database role using the roleName option. This is useful for testing Row-Level Security (RLS) policies with non-privileged roles.
// Connect as a specific role (must exist in parent branch)
neonTesting({
roleName: "test_user",
});Setting up a role for RLS testing:
Create a role in your parent branch without
BYPASSRLS:CREATE ROLE test_user WITH LOGIN PASSWORD 'your_password'; GRANT CONNECT ON DATABASE neondb TO test_user; GRANT USAGE ON SCHEMA public TO test_user; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO test_user;The role is automatically copied to test branches when branching.
Use the role in your tests:
neonTesting({ roleName: "test_user" }); test("RLS policy enforces user isolation", async () => { const sql = neon(process.env.DATABASE_URL!); // Now connected as test_user - RLS policies are enforced });
Continuous integration
It's easy to run Neon integration tests in CI/CD pipelines:
- GitHub Actions — see the example workflow
- Vercel — either
- add
vitest runto thebuildscript in package.json, or - add
vitest runto the Build Command in the Vercel dashboard
- add
API Reference
Main exports (neon-testing)
makeNeonTesting(options)
The factory function that creates a configured lifecycle function for your tests. See Configuration for available options.
// neon-testing.ts
import { makeNeonTesting } from "neon-testing";
export const neonTesting = makeNeonTesting({
apiKey: process.env.NEON_API_KEY!,
projectId: process.env.NEON_PROJECT_ID!,
});The configured function has the following properties:
.api
Access the Neon API client to make additional API calls:
import { neonTesting } from "./neon-testing";
const { data } = await neonTesting.api.getProjectBranch(projectId, branchId);See the Neon API client documentation for all available methods.
.deleteAllTestBranches()
Deletes all test branches from your Neon project. This is useful for cleanup when tests fail unexpectedly and leave orphaned test branches.
import { neonTesting } from "./neon-testing";
await neonTesting.deleteAllTestBranches();The function identifies test branches by looking for the integration-test: true annotation that Neon Testing automatically adds to all test branches it creates.
Accessing branch information
When you call the configured lifecycle function in your test files, it returns a function that gives you access to the current test branch:
import { neonTesting } from "./neon-testing";
const getBranch = neonTesting();
test("access branch information", () => {
const branch = getBranch();
console.log(branch.id);
console.log(branch.project_id);
console.log(branch.expires_at);
});See the Neon Branch API documentation for all available properties.
Vite plugin (neon-testing/vite)
The Vite plugin clears any existing DATABASE_URL environment variable before tests run, ensuring tests use isolated test databases.
import { defineConfig } from "vitest/config";
import { neonTesting } from "neon-testing/vite";
export default defineConfig({
plugins: [neonTesting()],
});Options:
debug(boolean, default:false) - Enable debug logging
This plugin is recommended but not required. Without it, tests might accidentally use your existing DATABASE_URL instead of isolated test databases.
Utilities (neon-testing/utils)
createBarrier(count)
Creates a synchronization barrier that blocks until count callers have arrived, then releases all of them simultaneously. Useful for deterministically reproducing race conditions in concurrent database operations.
import { createBarrier } from "neon-testing/utils";
const barrier = createBarrier(2);
// In concurrent operations:
await barrier(); // blocks until both arrive, then releases simultaneouslySee the examples and read more about harnessing Postgres race conditions.
lazySingleton(factory)
Creates a lazy singleton from a factory function. This is useful for managing database connections efficiently:
import { lazySingleton } from "neon-testing/utils";
import { neon } from "@neondatabase/serverless";
const sql = lazySingleton(() => neon(process.env.DATABASE_URL!));
// The connection is only created when first called
test("database operations", async () => {
const users = await sql()`SELECT * FROM users`;
// ...
});Contributing
Contributions are welcome! Please open issues or pull requests on GitHub.
Environment
To run tests locally, create an .env file in the project root with these keys:
NEON_API_KEY="***"NEON_PROJECT_ID="***"
Create a free Neon project at neon.com to test with.
Release
Releases are published via CI when a version tag is pushed. Use these scripts to bump the version and trigger a release:
Stable releases:
bun run release:patch # 1.2.3 → 1.2.4
bun run release:minor # 1.2.3 → 1.3.0
bun run release:major # 1.2.3 → 2.0.0Beta releases:
bun run release:beta # Default: 1.2.3 → 1.2.4-beta.0, then 1.2.4-beta.1, etc.
bun run release:beta:patch # Start beta for patch: 1.2.3 → 1.2.4-beta.0
bun run release:beta:minor # Start beta for minor: 1.2.3 → 1.3.0-beta.0
bun run release:beta:major # Start beta for major: 1.2.3 → 2.0.0-beta.0Use release:beta for most beta releases. It bumps the patch version once when starting from stable, then increments the beta number for subsequent releases. Use release:beta:minor or release:beta:major only when starting a beta cycle for a larger version bump.
The scripts bump the version, create a git tag, and push to trigger CI. The command will abort if there are uncommitted changes.
Author
Hi, I'm Mikael Lirbank. I build robust, reliable, high-quality AI systems. I care deeply about quality—AI evals, robust test suites, clean data models, and clean architecture.
Need help building elegant systems? I'm happy to help.
