npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

neon-testing

v3.0.0

Published

A testing utility for seamless integration tests with Neon Postgres, for Vitest and Bun Test.

Downloads

12,170

Readme

Neon Testing

Integration tests npm version GitHub release License: MIT

A testing utility for seamless integration tests with Neon Postgres, for Vitest and Bun Test.

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

  • 🔄 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 - Ships compiled JavaScript with type declarations and source maps too
  • 🎯 ESM only - No CommonJS support

How it works

  1. Branch creation: Before tests run, a new Neon branch is created with a unique name
  2. Environment setup: DATABASE_URL is set to point to your test branch
  3. Test execution: Your tests run against the isolated database
  4. Cleanup: After tests complete, the branch is automatically deleted

Test isolation

Each test file runs against its own database instance (Neon branch), so test files are fully isolated from each other. Tests within a single file share that branch and run sequentially — individual tests within a file are intentionally not isolated.

Vitest and Bun differ in how they execute files. Vitest runs each file in its own process, in parallel, while Bun runs every file in a single shared process, one after another by default. File-level isolation is the same on both — each file gets its own branch — but Vitest creates those branches concurrently whereas Bun creates them one at a time.

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

Install

# Vitest
bun add -d neon-testing vitest

# Bun Test
bun add -d neon-testing

Minimal example

// minimal.test.ts
import { expect, test } from "vitest";
import { makeNeonTesting } from "neon-testing/vitest";
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
  // (Vitest only)
  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. Setup

Register neon-testing/setup so any existing DATABASE_URL is cleared before tests run. A test file that forgets to enable branching then fails loudly instead of silently writing to a real database.

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    setupFiles: ["neon-testing/setup"],
  },
});

For Bun, add it to bunfig.toml instead:

[test]
preload = ["neon-testing/setup"]

Recommended but not required. Without it, a test file that forgets to call neonTesting() could fall back to your real DATABASE_URL (from .env or the environment) instead of an isolated test branch.

2. Configuration

Use the makeNeonTesting factory to generate a lifecycle function for your tests.

// neon-testing.ts
import { makeNeonTesting } from "neon-testing/vitest"; // or "neon-testing/bun"

// 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
  // (Vitest only)
  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.

Closing connections

Some drivers keep a connection open after a test finishes — Neon's WebSocket driver and node-postgres. If the test branch is deleted while a connection is still open, the driver errors. Avoid it in one of three ways:

  • close the connection yourself (e.g. await pool.end())
  • enable autoCloseWebSockets (WebSocket driver only, Vitest only)
  • keep the branch with deleteBranch: false

Neon's HTTP driver and Postgres.js close their own connections, so they need none of this.

Examples

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(); every option except apiKey can also be overridden per test file when calling the returned function.

export interface MakeNeonTestingOptions {
  /**
   * 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
   *
   * Vitest only.
   */
  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;
  /**
   * Override the `sslmode` query param on the connection URI (default:
   * undefined — URI is passed through unchanged)
   *
   * Neon's API returns URIs with `sslmode=require`. In pg v9 the meaning of
   * `require` changes to libpq semantics (encrypt, but don't verify the CA).
   *
   * - `"verify-full"` — strict CA verification (silences the pg v9 warning)
   * - `"require"` — preserves today's effective behavior under pg v9 by also
   *   setting `uselibpqcompat=true`
   *
   * Only affects drivers that parse `sslmode` (e.g. `pg`). The Neon
   * serverless driver ignores it.
   */
  sslMode?: "verify-full" | "require";
}

Base configuration

Configure the base settings in makeNeonTesting():

// neon-testing.ts
import { makeNeonTesting } from "neon-testing/vitest";

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:

  1. 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;
  2. The role is automatically copied to test branches when branching.

  3. 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
    });

SSL mode

Neon returns URIs with sslmode=require. In pg v9 the meaning of require changes to libpq semantics (encrypt, but don't verify the CA). Use sslMode to pick the semantics you want:

// Strict CA verification (silences the pg v9 deprecation warning) - recommended
neonTesting({ sslMode: "verify-full" });

// Preserve today's effective behavior under pg v9
neonTesting({ sslMode: "require" });

Only affects drivers that parse sslmode (e.g. pg). The Neon serverless driver ignores it.

Error handling

Rate limiting

Neon Testing automatically retries with exponential backoff when the Neon API returns HTTP 423 (resource locked), up to 8 attempts. No action needed from the user.

API errors

Other Neon API errors are surfaced with the API's error code and message so you can see what went wrong. For example:

Neon API error - HTTP 422 - ROOT_BRANCHES_LIMIT_EXCEEDED - root branches limit exceeded

The original error is preserved on .cause for programmatic access to the full API response.

Schema-only branch limits

Schema-only branches (schemaOnly: true) are root branches in Neon, which have a per-project limit. If you hit ROOT_BRANCHES_LIMIT_EXCEEDED, reduce the number of concurrent test files (see Vitest's parallelism docs) or contact Neon to increase the limit.

Continuous integration

It's easy to run Neon integration tests in CI/CD pipelines:

  • GitHub Actions — see the example workflow
  • Vercel — either
    • add vitest run to the build script in package.json, or
    • add vitest run to the Build Command in the Vercel dashboard

API Reference

Runner entry (neon-testing/vitest or neon-testing/bun)

Both entries export the same makeNeonTesting; they differ only in which runner's beforeAll/afterAll hooks they wire up. (For other runners, neon-testing/core takes the hooks directly.)

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/vitest";

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.

Setup (neon-testing/setup)

A zero-config preload that clears DATABASE_URL before tests run, so a file that forgets to call neonTesting() can't reach a real database. Register it with your runner — Vitest setupFiles or Bun bunfig preload (see Recommended usage).

Set NEON_TESTING_DEBUG=true to log when it clears a value.

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 simultaneously

See 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.0
bun run release:stable  # 2.0.0-beta.1 → 2.0.0

Beta 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.0

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

Use release:stable to promote the current beta to its stable version. It removes the prerelease suffix, so 1.2.4-beta.0 becomes 1.2.4, 1.3.0-beta.0 becomes 1.3.0, and 3.0.0-beta.1 becomes 3.0.0.

For example, a breaking-change cycle from 2.7.0 to 3.0.0 looks like:

bun run release:beta:major # 2.7.0 → 3.0.0-beta.0
bun run release:beta       # 3.0.0-beta.0 → 3.0.0-beta.1
bun run release:stable     # 3.0.0-beta.1 → 3.0.0

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.