|
1 | | -pysql |
| 1 | +# pysql-test |
| 2 | + |
| 3 | +PostgreSQL testing framework for Python - instant, isolated databases with automatic transaction rollback. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Instant isolated databases**: Each test gets a fresh database with a unique UUID name |
| 8 | +- **Transaction rollback**: Changes are automatically rolled back after each test |
| 9 | +- **Composable seeding**: Seed your database with SQL files, custom functions, or combine multiple strategies |
| 10 | +- **RLS testing support**: Easy context switching for testing Row Level Security policies |
| 11 | +- **Clean API**: Simple, intuitive interface inspired by the Node.js pgsql-test library |
| 12 | + |
| 13 | +## Installation |
| 14 | + |
| 15 | +```bash |
| 16 | +# Using Poetry (recommended) |
| 17 | +poetry add pysql-test |
| 18 | + |
| 19 | +# Using pip |
| 20 | +pip install pysql-test |
| 21 | +``` |
| 22 | + |
| 23 | +## Quick Start |
| 24 | + |
| 25 | +```python |
| 26 | +import pytest |
| 27 | +from pysql_test import get_connections, seed |
| 28 | + |
| 29 | +# Basic usage |
| 30 | +def test_basic_query(): |
| 31 | + conn = get_connections() |
| 32 | + result = conn.db.query('SELECT 1 as value') |
| 33 | + assert result.rows[0]['value'] == 1 |
| 34 | + conn.teardown() |
| 35 | + |
| 36 | +# With pytest fixture |
| 37 | +@pytest.fixture |
| 38 | +def db(): |
| 39 | + conn = get_connections() |
| 40 | + yield conn.db |
| 41 | + conn.teardown() |
| 42 | + |
| 43 | +def test_with_fixture(db): |
| 44 | + result = db.query('SELECT 1 as value') |
| 45 | + assert result.rows[0]['value'] == 1 |
| 46 | + |
| 47 | +# With SQL file seeding |
| 48 | +@pytest.fixture |
| 49 | +def seeded_db(): |
| 50 | + conn = get_connections( |
| 51 | + seed_adapters=[seed.sqlfile(['schema.sql', 'fixtures.sql'])] |
| 52 | + ) |
| 53 | + yield conn.db |
| 54 | + conn.teardown() |
| 55 | + |
| 56 | +def test_with_seeding(seeded_db): |
| 57 | + users = seeded_db.many('SELECT * FROM users') |
| 58 | + assert len(users) > 0 |
| 59 | +``` |
| 60 | + |
| 61 | +## Transaction Isolation |
| 62 | + |
| 63 | +Use `before_each()` and `after_each()` for per-test isolation: |
| 64 | + |
| 65 | +```python |
| 66 | +@pytest.fixture |
| 67 | +def db(): |
| 68 | + conn = get_connections( |
| 69 | + seed_adapters=[seed.sqlfile(['schema.sql'])] |
| 70 | + ) |
| 71 | + db = conn.db |
| 72 | + db.before_each() # Begin transaction + savepoint |
| 73 | + yield db |
| 74 | + db.after_each() # Rollback to savepoint |
| 75 | + conn.teardown() |
| 76 | + |
| 77 | +def test_insert_user(db): |
| 78 | + # This insert will be rolled back after the test |
| 79 | + db.execute("INSERT INTO users (name) VALUES ('Test User')") |
| 80 | + result = db.one("SELECT * FROM users WHERE name = 'Test User'") |
| 81 | + assert result['name'] == 'Test User' |
| 82 | + |
| 83 | +def test_user_count(db): |
| 84 | + # Previous test's insert is not visible here |
| 85 | + result = db.one("SELECT COUNT(*) as count FROM users") |
| 86 | + assert result['count'] == 0 # Only seeded data |
| 87 | +``` |
| 88 | + |
| 89 | +## RLS Testing |
| 90 | + |
| 91 | +Test Row Level Security policies by switching contexts: |
| 92 | + |
| 93 | +```python |
| 94 | +def test_rls_policy(db): |
| 95 | + db.before_each() |
| 96 | + |
| 97 | + # Set the user context |
| 98 | + db.set_context({'app.user_id': '123'}) |
| 99 | + |
| 100 | + # Now queries will be filtered by RLS policies |
| 101 | + result = db.many('SELECT * FROM user_data') |
| 102 | + |
| 103 | + db.after_each() |
| 104 | +``` |
| 105 | + |
| 106 | +## Seeding Strategies |
| 107 | + |
| 108 | +### SQL Files |
| 109 | + |
| 110 | +```python |
| 111 | +seed.sqlfile(['schema.sql', 'fixtures.sql']) |
| 112 | +``` |
| 113 | + |
| 114 | +### Custom Functions |
| 115 | + |
| 116 | +```python |
| 117 | +seed.fn(lambda ctx: ctx['pg'].execute( |
| 118 | + "INSERT INTO users (name) VALUES (%s)", ('Alice',) |
| 119 | +)) |
| 120 | +``` |
| 121 | + |
| 122 | +### Composed Seeding |
| 123 | + |
| 124 | +```python |
| 125 | +seed.compose([ |
| 126 | + seed.sqlfile(['schema.sql']), |
| 127 | + seed.fn(lambda ctx: ctx['pg'].execute("INSERT INTO ...")), |
| 128 | +]) |
| 129 | +``` |
| 130 | + |
| 131 | +## Configuration |
| 132 | + |
| 133 | +Configure via environment variables: |
| 134 | + |
| 135 | +```bash |
| 136 | +export PGHOST=localhost |
| 137 | +export PGPORT=5432 |
| 138 | +export PGUSER=postgres |
| 139 | +export PGPASSWORD=your_password |
| 140 | +``` |
| 141 | + |
| 142 | +Or pass configuration directly: |
| 143 | + |
| 144 | +```python |
| 145 | +conn = get_connections( |
| 146 | + pg_config={ |
| 147 | + 'host': 'localhost', |
| 148 | + 'port': 5432, |
| 149 | + 'user': 'postgres', |
| 150 | + 'password': 'your_password', |
| 151 | + } |
| 152 | +) |
| 153 | +``` |
| 154 | + |
| 155 | +## API Reference |
| 156 | + |
| 157 | +### `get_connections(pg_config?, connection_options?, seed_adapters?)` |
| 158 | + |
| 159 | +Creates a new isolated test database and returns connection objects. |
| 160 | + |
| 161 | +Returns a `ConnectionResult` with: |
| 162 | +- `pg`: PgTestClient connected as superuser |
| 163 | +- `db`: PgTestClient for testing (same as pg for now) |
| 164 | +- `admin`: DbAdmin for database management |
| 165 | +- `manager`: PgTestConnector managing connections |
| 166 | +- `teardown()`: Function to clean up |
| 167 | + |
| 168 | +### `PgTestClient` |
| 169 | + |
| 170 | +- `query(sql, params?)`: Execute SQL and return QueryResult |
| 171 | +- `one(sql, params?)`: Return exactly one row |
| 172 | +- `one_or_none(sql, params?)`: Return one row or None |
| 173 | +- `many(sql, params?)`: Return multiple rows |
| 174 | +- `many_or_none(sql, params?)`: Return rows (may be empty) |
| 175 | +- `execute(sql, params?)`: Execute and return affected row count |
| 176 | +- `before_each()`: Start test isolation (transaction + savepoint) |
| 177 | +- `after_each()`: End test isolation (rollback) |
| 178 | +- `set_context(dict)`: Set session variables for RLS testing |
| 179 | + |
| 180 | +## Development |
| 181 | + |
| 182 | +```bash |
| 183 | +# Install dependencies |
| 184 | +poetry install |
| 185 | + |
| 186 | +# Run tests |
| 187 | +poetry run pytest |
| 188 | + |
| 189 | +# Run linting |
| 190 | +poetry run ruff check . |
| 191 | + |
| 192 | +# Run type checking |
| 193 | +poetry run mypy src |
| 194 | +``` |
| 195 | + |
| 196 | +## License |
| 197 | + |
| 198 | +MIT |
0 commit comments