Pytest plugin for Neon database branch isolation in tests
Project description
pytest-neon
A pytest plugin that provides Neon database branches for integration testing.
Features
- Automatic branch management: Creates a test branch at session start, deletes at end
- Branch expiry: Auto-cleanup via 10-minute expiry (crash-safe)
- Migration support: Run migrations once, all tests share the migrated schema
- pytest-xdist support: All workers share a single branch
- Minimal API calls: Single branch creation reduces rate limiting issues
Installation
pip install pytest-neon
# With optional database drivers
pip install pytest-neon[psycopg] # psycopg v3 support
pip install pytest-neon[psycopg2] # psycopg2 support
pip install pytest-neon[sqlalchemy] # SQLAlchemy engine support
Quick Start
- Set environment variables:
export NEON_API_KEY="your-api-key"
export NEON_PROJECT_ID="your-project-id"
- Use the
neon_branchfixture in your tests:
def test_query_users(neon_branch):
import psycopg
with psycopg.connect(neon_branch.connection_string) as conn:
result = conn.execute("SELECT * FROM users").fetchall()
assert len(result) >= 0
The DATABASE_URL environment variable is automatically set when the fixture is active.
Fixtures
neon_branch (session-scoped)
The main fixture providing a shared Neon branch for all tests.
def test_example(neon_branch):
# neon_branch.branch_id - Neon branch ID
# neon_branch.project_id - Neon project ID
# neon_branch.connection_string - PostgreSQL connection string
# neon_branch.host - Database host
pass
Important: All tests share the same branch. Data written by one test is visible to subsequent tests. See Test Isolation for patterns to handle this.
neon_apply_migrations (session-scoped)
Override this fixture to run migrations before tests:
# conftest.py
@pytest.fixture(scope="session")
def neon_apply_migrations(_neon_test_branch):
"""Run database migrations."""
import subprocess
subprocess.run(["alembic", "upgrade", "head"], check=True)
Or with Django:
@pytest.fixture(scope="session")
def neon_apply_migrations(_neon_test_branch):
from django.core.management import call_command
call_command("migrate", "--noinput")
Or with raw SQL:
@pytest.fixture(scope="session")
def neon_apply_migrations(_neon_test_branch):
import psycopg
branch, is_creator = _neon_test_branch
with psycopg.connect(branch.connection_string) as conn:
with open("schema.sql") as f:
conn.execute(f.read())
conn.commit()
Connection Fixtures (Optional)
These require extra dependencies:
neon_connection - psycopg2 connection (requires pytest-neon[psycopg2])
def test_insert(neon_connection):
cur = neon_connection.cursor()
cur.execute("INSERT INTO users (name) VALUES (%s)", ("test",))
neon_connection.commit()
neon_connection_psycopg - psycopg v3 connection (requires pytest-neon[psycopg])
def test_insert(neon_connection_psycopg):
with neon_connection_psycopg.cursor() as cur:
cur.execute("INSERT INTO users (name) VALUES ('test')")
neon_connection_psycopg.commit()
neon_engine - SQLAlchemy engine (requires pytest-neon[sqlalchemy])
def test_query(neon_engine):
from sqlalchemy import text
with neon_engine.connect() as conn:
result = conn.execute(text("SELECT 1"))
Test Isolation
Since all tests share a single branch, you may need to handle test isolation yourself. Here are recommended patterns:
Transaction Rollback (Recommended)
@pytest.fixture
def db_transaction(neon_branch):
"""Provide a database transaction that rolls back after each test."""
import psycopg
conn = psycopg.connect(neon_branch.connection_string)
conn.execute("BEGIN")
yield conn
conn.execute("ROLLBACK")
conn.close()
def test_insert(db_transaction):
db_transaction.execute("INSERT INTO users (name) VALUES ('test')")
# Automatically rolled back - next test won't see this
Table Truncation
@pytest.fixture(autouse=True)
def clean_tables(neon_branch):
"""Clean up test data after each test."""
yield
import psycopg
with psycopg.connect(neon_branch.connection_string) as conn:
conn.execute("TRUNCATE users, orders CASCADE")
conn.commit()
Unique Identifiers
import uuid
def test_create_user(neon_branch):
unique_id = uuid.uuid4().hex[:8]
email = f"test_{unique_id}@example.com"
# Create user with unique email - no conflicts with other tests
Configuration
Environment Variables
| Variable | Description |
|---|---|
NEON_API_KEY |
Neon API key (required) |
NEON_PROJECT_ID |
Neon project ID (required) |
NEON_PARENT_BRANCH_ID |
Parent branch to create test branches from |
NEON_DATABASE |
Database name (default: neondb) |
NEON_ROLE |
Database role (default: neondb_owner) |
Command Line Options
pytest --neon-api-key=KEY --neon-project-id=ID
pytest --neon-parent-branch=BRANCH_ID
pytest --neon-database=mydb --neon-role=myrole
pytest --neon-keep-branches # Don't delete branches (for debugging)
pytest --neon-branch-expiry=600 # Branch expiry in seconds (default: 600)
pytest --neon-env-var=CUSTOM_URL # Use custom env var instead of DATABASE_URL
pytest.ini / pyproject.toml
[pytest]
neon_api_key = your-api-key
neon_project_id = your-project-id
neon_parent_branch = br-parent-123
neon_database = mydb
neon_role = myrole
neon_keep_branches = false
neon_branch_expiry = 600
neon_env_var = DATABASE_URL
Architecture
Parent Branch (configured or project default)
└── Test Branch (session-scoped, 10-min expiry)
↑ migrations run here ONCE, all tests share this
The plugin creates exactly one branch per test session:
- First test triggers branch creation with auto-expiry
- Migrations run once (if
neon_apply_migrationsis overridden) - All tests share the same branch
- Branch deleted at session end (plus auto-expiry as safety net)
pytest-xdist Support
When running with pytest-xdist, all workers share the same branch:
- First worker creates the branch and runs migrations
- Other workers wait for migrations to complete
- All workers see the same database state
pytest -n 4 # 4 workers, all sharing one branch
Branch Naming
Branches are automatically named to help identify their source:
pytest-[git-branch]-[random]-test
Examples:
pytest-main-a1b2-test- Test branch frommainpytest-feature-auth-c3d4-test- Test branch fromfeature/authpytest-a1b2-test- When not in a git repo
The git branch name is sanitized (only a-z, 0-9, -, _ allowed) and truncated to 15 characters.
Upgrading from v2.x
Version 3.0 simplifies the plugin significantly. If you're upgrading from v2.x:
Removed Fixtures
These fixtures have been removed:
neon_branch_readonly→ useneon_branchneon_branch_readwrite→ useneon_branchneon_branch_isolated→ useneon_branch+ transaction rollbackneon_branch_dirty→ useneon_branchneon_branch_shared→ useneon_branch
Migration Hook Change
The migration hook now uses _neon_test_branch instead of _neon_migration_branch:
# Before (v2.x)
@pytest.fixture(scope="session")
def neon_apply_migrations(_neon_migration_branch):
...
# After (v3.x)
@pytest.fixture(scope="session")
def neon_apply_migrations(_neon_test_branch):
...
No Per-Test Reset
The v2.x neon_branch_isolated fixture reset the branch after each test. In v3.x, there's no automatic reset. Use transaction rollback or cleanup fixtures for test isolation.
Troubleshooting
Rate Limiting
The plugin includes automatic retry with exponential backoff for Neon API rate limits. If you're hitting rate limits:
- The plugin creates only 1-2 API calls per session (create + delete)
- Consider increasing
--neon-branch-expiryto reduce cleanup calls
Stale Connections (SQLAlchemy)
If using SQLAlchemy with connection pooling, use pool_pre_ping=True:
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
This is a best practice for any cloud database where connections can be terminated externally.
Branch Not Deleted
If a test run crashes, the branch auto-expires after 10 minutes (configurable). You can also use --neon-keep-branches to prevent deletion for debugging.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_neon-3.0.0.tar.gz.
File metadata
- Download URL: pytest_neon-3.0.0.tar.gz
- Upload date:
- Size: 130.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6617fe1446de1ba703d8cfc11c153cbdf485d6016d26d9696116497ffd82d6ed
|
|
| MD5 |
51f2983020d47b902e929c7be6ebeaf6
|
|
| BLAKE2b-256 |
4745f9ffeb9c976834af3ae46f00d5730831ae67c438a08cd0506946dc49df51
|
File details
Details for the file pytest_neon-3.0.0-py3-none-any.whl.
File metadata
- Download URL: pytest_neon-3.0.0-py3-none-any.whl
- Upload date:
- Size: 16.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2cbaf5a5a5666b0882cd1e027a07b473ae64988fc2b8ff6b2fbbf8d67ae4930d
|
|
| MD5 |
2834c8a97c6eafe5be5ee590d3224539
|
|
| BLAKE2b-256 |
38cf5c335eba19744dcaf60b81ca107f36a75fa4fe85343de498922b51e73c28
|