docs: add SQLAlchemy fixture best practices to testing guide
Add comprehensive section on SQLAlchemy fixture best practices: - Explain why db.expunge() is an anti-pattern in test fixtures - Document how test isolation works via the db fixture - Provide examples of correct vs incorrect fixture patterns - Show how to use db.refresh() for fresh data instead of expunge This documents the lessons learned from the fixture refactoring and helps prevent future issues with DetachedInstanceError. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -893,7 +893,78 @@ def test_delete_user(db):
|
||||
delete_user(db, user_id) # Depends on previous test
|
||||
```
|
||||
|
||||
### 2. Arrange-Act-Assert Pattern
|
||||
### 2. SQLAlchemy Fixture Best Practices
|
||||
|
||||
**IMPORTANT**: Follow these rules when working with SQLAlchemy fixtures to avoid common pitfalls:
|
||||
|
||||
#### ❌ Never Use `db.expunge()` in Fixtures
|
||||
|
||||
```python
|
||||
# ❌ BAD - Causes lazy loading errors
|
||||
@pytest.fixture
|
||||
def test_user(db, auth_manager):
|
||||
user = User(email="test@example.com", ...)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
db.expunge(user) # ❌ NEVER DO THIS!
|
||||
return user
|
||||
|
||||
# When you later try to access relationships:
|
||||
# user.company.name # ❌ DetachedInstanceError!
|
||||
```
|
||||
|
||||
```python
|
||||
# ✅ GOOD - Keep objects attached to session
|
||||
@pytest.fixture
|
||||
def test_user(db, auth_manager):
|
||||
user = User(email="test@example.com", ...)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user # ✅ Object stays attached to session
|
||||
```
|
||||
|
||||
#### Why `db.expunge()` Is an Anti-Pattern
|
||||
|
||||
1. **Breaks lazy loading**: Detached objects cannot access relationships like `user.company` or `product.marketplace_product`
|
||||
2. **Causes DetachedInstanceError**: SQLAlchemy throws errors when accessing unloaded relationships on detached objects
|
||||
3. **Test isolation is already handled**: The `db` fixture drops and recreates all tables after each test
|
||||
|
||||
#### How Test Isolation Works
|
||||
|
||||
The `db` fixture in `tests/conftest.py` provides isolation by:
|
||||
- Creating fresh tables before each test
|
||||
- Dropping all tables after each test completes
|
||||
- Using `expire_on_commit=False` to prevent objects from expiring after commits
|
||||
|
||||
```python
|
||||
@pytest.fixture(scope="function")
|
||||
def db(engine, testing_session_local):
|
||||
Base.metadata.create_all(bind=engine) # Fresh tables
|
||||
db_session = testing_session_local()
|
||||
|
||||
try:
|
||||
yield db_session
|
||||
finally:
|
||||
db_session.close()
|
||||
Base.metadata.drop_all(bind=engine) # Clean up
|
||||
Base.metadata.create_all(bind=engine) # Ready for next test
|
||||
```
|
||||
|
||||
#### If You Need Fresh Data
|
||||
|
||||
Use `db.refresh(obj)` instead of expunge/re-query patterns:
|
||||
|
||||
```python
|
||||
# ✅ GOOD - Refresh for fresh data
|
||||
def test_update_user(db, test_user):
|
||||
update_user_email(db, test_user.id, "new@example.com")
|
||||
db.refresh(test_user) # Get latest data from database
|
||||
assert test_user.email == "new@example.com"
|
||||
```
|
||||
|
||||
### 3. Arrange-Act-Assert Pattern
|
||||
|
||||
Structure tests clearly using AAA pattern:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user