Skip to content

peewee-testing

**Scope**: In-memory SQLite databases, test isolation, fixture patterns, and migration testing with Peewee ORM. **Version range**: Peewee 3.x, Python 3.8+, pytest 7+ **Generated**: 2026-04-14 — verify against current pytest-peewee releases

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

**Scope**: In-memory SQLite databases, test isolation, fixture patterns, and migration testing with Peewee ORM. **Version range**: Peewee 3.x, Python 3.8+, pytest 7+ **Generated**: 2026-04-14 — verify against current pytest-peewee releases

Agent definition

peewee-testing.md

Peewee Testing Patterns

> **Scope**: In-memory SQLite databases, test isolation, fixture patterns, and migration testing with Peewee ORM. > **Version range**: Peewee 3.x, Python 3.8+, pytest 7+ > **Generated**: 2026-04-14 — verify against current pytest-peewee releases

---

Overview

Critical failure mode: shared database state bleeds between tests with module-level db instances. Fix: `:memory:` SQLite per-test via `bind_ctx()`. Works cleanly with pytest fixtures.

---

Pattern Table

| Pattern | Use When | Avoid When | |---------|----------|------------| | `SqliteDatabase(':memory:')` | Unit tests, fast isolation | Integration tests needing file persistence | | `db.bind_ctx(models)` | Per-test isolation via fixture | Module-level setup (state leaks) | | `db.create_tables(models)` | Schema creation in fixture | Production code (use migrations) | | `TestModel.truncate_table()` | Clearing data between tests | Full isolation (use bind_ctx instead) | | `atomic()` rollback | Preserving db state across tests | Tests that commit intentionally |

---

Correct Patterns

Per-Test Isolation with `bind_ctx()`

Fresh in-memory db per test. Models re-bind at test start, dropped at end.

import pytest
from peewee import SqliteDatabase
from myapp.models import User, Post, Comment

ALL_MODELS = [User, Post, Comment]

@pytest.fixture
def db():
    """Fresh in-memory database for each test."""
    test_db = SqliteDatabase(':memory:', pragmas={'foreign_keys': 1})
    with test_db.bind_ctx(ALL_MODELS):
        test_db.create_tables(ALL_MODELS)
        yield test_db
        # Tables auto-dropped when in-memory db is garbage collected

def test_user_creation(db):
    user = User.create(username='alice', email='alice@example.com')
    assert User.select().count() == 1
    assert user.id is not None

def test_post_belongs_to_user(db):
    user = User.create(username='bob', email='bob@example.com')
    post = Post.create(user=user, title='Hello')
    # FK constraint enforced because foreign_keys=1 pragma is set
    assert post.user_id == user.id

**Why**: `bind_ctx()` temporarily rebinds models to test db without changing `Meta.database`. Thread-safe, works with parallel execution.

---

Fixture Factories for Related Data

Factory functions avoid repetitive setup:

@pytest.fixture
def make_user(db):
    """Factory for creating users with defaults."""
    def factory(username='testuser', email=None, **kwargs):
        email = email or f'{username}@example.com'
        return User.create(username=username, email=email, **kwargs)
    return factory

@pytest.fixture
def make_post(db, make_user):
    """Factory for creating posts, creates user if not provided."""
    def factory(title='Test Post', user=None, **kwargs):
        if user is None:
            user = make_user()
        return Post.create(title=title, user=user, **kwargs)
    return factory

def test_post_count_per_user(db, make_user, make_post):
    alice = make_user('alice')
    make_post(user=alice)
    make_post(user=alice)
    make_post(user=alice)

    # Verify the prefetch path works correctly
    users = User.select().prefetch(Post)
    user = [u for u in users if u.username == 'alice'][0]
    assert len(list(user.posts)) == 3

---

Testing Transactions and Rollback

def test_atomic_rollback_on_error(db):
    """Verify atomic() rolls back all changes when exception raised."""
    user = User.create(username='alice', email='alice@example.com')

    with pytest.raises(ValueError):
        with db.atomic():
            Post.create(user=user, title='First post')
            Post.create(user=user, title='Second post')
            raise ValueError('intentional rollback')

    # Both posts should be rolled back
    assert Post.select().count() == 0

def test_savepoint_partial_rollback(db):
    """Verify nested atomic() uses savepoints for partial rollback."""
    user = User.create(username='alice', email='alice@example.com')

    with db.atomic():
        Post.create(user=user, title='Outer post')
        try:
            with db.atomic():  # Creates savepoint
                Post.create(user=user, title='Inner post')
                raise ValueError('rollback inner only')
        except ValueError:
            pass  # Inner savepoint rolled back, outer continues

    # Only the outer post committed
    assert Post.select().count() == 1
    assert Post.get().title == 'Outer post'

---

Testing Migration Scripts

import pytest
from peewee import SqliteDatabase, Model, CharField, TextField
from playhouse.migrate import SqliteMigrator, migrate

@pytest.fixture
def pre_migration_db():
    """Database in the state before a migration runs."""
    db = SqliteDatabase(':memory:')

    class OldUser(Model):
        username = CharField()
        # No 'email' column yet — simulates pre-migration state
        class Meta:
            database = db
            table_name = 'user'

    db.create_tables([OldUser])
    OldUser.create(username='alice')
    yield db

def test_add_email_column_migration(pre_migration_db):
    migrator = SqliteMigrator(pre_migration_db)
    migrate(
        migrator.add_column('user', 'email', TextField(null=True))
    )

    # Verify column exists and existing rows have NULL
    cursor = pre_migration_db.execute_sql('SELECT email FROM user')
    rows = cursor.fetchall()
    assert len(rows) == 1
    assert rows[0][0] is None  # NULL for pre-existing rows

---

Pattern Catalog

<!-- no-pair-required: section header with no content -->

Use Per-Test Database Fixtures (State Leakage)

**Detection**:

# Find test files using module-level database setup
grep -rn 'db = SqliteDatabase' --include="test_*.py"
grep -rn 'setUpClass\|setup_module' --include="test_*.py" -A 5 | grep 'create_tables'
rg 'SqliteDatabase.*test' --type py | grep -v 'fixture\|conftest'

**Preferred action:** Use a per-test `db` fixture with `bind_ctx()` so each test gets a f

Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked