Fixture Reuse¶
rpytest can reuse session-scoped fixtures across multiple test runs, dramatically reducing setup time.
The Problem¶
Session fixtures are expensive:
@pytest.fixture(scope="session")
def database():
db = create_database() # 30 seconds
load_test_data(db) # 10 seconds
yield db
db.close()
Every rpytest invocation recreates this fixture, even if nothing changed.
The Solution¶
Enable fixture reuse:
The daemon keeps session fixtures alive between runs.
How It Works¶
Run 1: rpytest tests/ --reuse-fixtures
→ Create database fixture (40s)
→ Run tests (10s)
→ Keep fixture in daemon
Run 2: rpytest tests/ --reuse-fixtures
→ Reuse database fixture (0s)
→ Run tests (10s)
Run 3: rpytest tests/ --reuse-fixtures
→ Reuse database fixture (0s)
→ Run tests (10s)
Configuration¶
Enable by Default¶
Command Line¶
# Enable
rpytest tests/ --reuse-fixtures
# Set max age
rpytest tests/ --reuse-fixtures --fixture-max-age=1800
Fixture Invalidation¶
Fixtures are invalidated when:
1. conftest.py Changes¶
2. Max Age Exceeded¶
3. Explicit Cleanup¶
4. Daemon Restart¶
Session Status¶
View active fixtures:
Output:
Session Status
==============
Session ID: sess-abc123
Repo: /path/to/project
Created: 2024-01-15 10:30:00
Last Run: 2024-01-15 10:45:00
Total Runs: 5
Active Session Fixtures:
database
Age: 15 minutes
Created: 2024-01-15 10:30:00
Reused: 4 times
redis_client
Age: 15 minutes
Created: 2024-01-15 10:30:00
Reused: 4 times
Fixture Reuse: ENABLED
Max Age: 600 seconds
Supported Scopes¶
| Scope | Reusable | Notes |
|---|---|---|
session |
✅ | Fully supported |
package |
✅ | Reused within package |
module |
❌ | Too granular |
class |
❌ | Too granular |
function |
❌ | Never reused |
Best Practices¶
1. Design for Reuse¶
@pytest.fixture(scope="session")
def database(request):
db = create_database()
def cleanup():
# Clean test data, keep structure
db.execute("TRUNCATE ALL TABLES")
request.addfinalizer(cleanup)
return db
2. Use Transactions for Isolation¶
@pytest.fixture(scope="session")
def database():
return create_database()
@pytest.fixture
def db_session(database):
# Each test gets isolated transaction
session = database.begin_transaction()
yield session
session.rollback()
3. Handle State Carefully¶
@pytest.fixture(scope="session")
def api_client():
client = APIClient()
return client
@pytest.fixture
def fresh_api_client(api_client):
# Reset state for each test
api_client.clear_cache()
api_client.reset_rate_limits()
return api_client
Caveats¶
State Leakage¶
If fixtures maintain state, it can leak between runs:
# Dangerous: Accumulates state
@pytest.fixture(scope="session")
def counter():
return {"count": 0}
def test_increment(counter):
counter["count"] += 1
assert counter["count"] == 1 # May fail on rerun!
Solution:
External Dependencies¶
External services may not match fixture state:
@pytest.fixture(scope="session")
def external_api_state():
setup_external_api()
return get_api_state()
# If external API resets, fixture is stale
Solution:
@pytest.fixture(scope="session")
def external_api_state():
state = get_api_state()
if not state.is_valid():
setup_external_api()
state = get_api_state()
return state
Memory Usage¶
Long-running sessions accumulate memory:
# Monitor daemon memory
rpytest --daemon-status
# Restart if needed
rpytest --daemon-stop
rpytest tests/ --reuse-fixtures
CI/CD Considerations¶
Fixture Cache Between Jobs¶
For multi-shard CI, each runner has its own daemon:
# Fixtures created per-runner
jobs:
test:
strategy:
matrix:
shard: [0, 1, 2, 3]
steps:
- run: rpytest tests/ --shard=${{ matrix.shard }} --reuse-fixtures
Persistent Daemon¶
For long CI pipelines:
- name: Start daemon
run: rpytest --daemon &
- name: Run unit tests
run: rpytest tests/unit/ --reuse-fixtures
- name: Run integration tests
run: rpytest tests/integration/ --reuse-fixtures
- name: Stop daemon
run: rpytest --daemon-stop
Troubleshooting¶
Fixtures Not Reusing¶
-
Check if enabled:
-
Check fixture age:
-
Check for conftest changes:
Stale Fixtures¶
If tests fail due to stale fixtures:
Memory Growth¶
If daemon uses too much memory: