Reviews Projects Services Benefits
Mikael Lirbank — (updated February 2026)

Simplify Postgres integration testing

A version of this article was published on the Neon blog.

Our tests pass, but our production deploy fails because of a unique constraint violation. Sound familiar? The problem isn't our code—it's that we're testing mocks instead of real database behavior, or our test database has drifted from production. Either way, our tests aren't reflecting what will actually happen when our code hits production.

To be confident our code behaves as expected, our tests need to run against a real database that's in sync with production. But that's easier said than done—spinning up containers, managing schema migrations, coordinating between dev environments, CI/CD pipelines, and production. Many teams either skip it entirely or settle for inadequate mocks.

That's where Neon branching and Neon Testing change everything. Neon gives you instant, isolated copies of your production database without infrastructure headache. Neon Testing turns those branches into disposable test environments, so your tests run against the same database constraints and behaviors as production.

The result? Reliable integration tests that are as easy as unit tests.

Getting started

Here's a user creation function that relies on a unique index to prevent duplicate emails.

// db/users.ts
export async function createUser(email: string) {
  return sql`INSERT INTO users (email) VALUES (${email})`;
}

If your users table has a unique constraint on email, calling this function twice with the same email should fail the second time. That's exactly the kind of behavior you can't mock, and a stale test database without the unique constraint would give you a false positive.

Let's test this!

Step 1: Set up

Install the packages.

bun add @neondatabase/serverless
bun add -D neon-testing vitest

Configure Vitest to ensure tests use isolated databases. This plugin clears any existing DATABASE_URL environment variable, preventing tests from accidentally using your local or production database instead of the isolated test branches.

// vitest.config.ts
import { defineConfig } from "vitest/config";
import { neonTesting } from "neon-testing/vite";

export default defineConfig({
  plugins: [neonTesting()],
});

Create a small setup module that you'll reuse across all your test files.

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

// Configure once (see npm docs for options)
export const neonTesting = makeNeonTesting({
  apiKey: process.env.NEON_API_KEY!,
  projectId: process.env.NEON_PROJECT_ID!,
  // Recommended for Neon WebSocket drivers to automatically close connections:
  autoCloseWebSockets: true,
});

Step 2: Write tests that verify real database behavior

Now you can test the actual constraint behavior against your real production schema (with or without production data). Each test file automatically gets its own fresh database clone on each run, so tests are completely isolated.

// db/users.test.ts
import { test, expect } from "vitest";
import { neonTesting } from "../neon-testing";
import { createUser } from "./users";

// Enable Neon Testing for this file
neonTesting();

test("unique email constraint", async () => {
  await createUser("test@example.com");
  await expect(createUser("test@example.com")).rejects.toThrow();
});

Step 3: Run your tests

Start Vitest in watch mode and see your tests run as you edit.

bunx vitest

That's it! Your tests now run against the same database constraints and behaviors as production.

Wrapping up

Integration testing usually fails teams, not because the tests are hard to write, but because the infrastructure is hard to stand up and maintain. Neon branching and Neon Testing take that off your plate. Now you can have reliable integration testing across your entire development lifecycle—local development, preview, staging, CI/CD, and production.

If integration testing has been slowing you down, give Neon Testing a try. You can find it on npm and GitHub.