Skip to main content

Smart fixture caching for pytest with SQLite storage

Project description

pytest-fixture-cache

Smart fixture caching for pytest - Speed up your test suite by caching expensive fixtures with SQLite storage.

PyPI version Python versions License: MIT

Why pytest-fixture-cache?

Testing with expensive fixtures (API calls, database seeding, file generation) can slow down your test suite significantly. This plugin automatically caches fixture results to SQLite, reducing test execution time by up to 10x for fixture-heavy test suites.

Key Features:

  • Drop-in decorator - Just add @cached_fixture to your fixtures
  • SQLite storage - Fast, reliable, zero-config persistence
  • Smart invalidation - Mark fixtures dirty when data changes
  • Scope-aware - Respects pytest fixture scopes (function/class/module/session)
  • CLI integration - Clear cache, disable caching, view stats
  • Result monad support - Works with returns, result libraries
  • Hit tracking - Monitor cache effectiveness with statistics

Installation

pip install pytest-fixture-cache

For Result monad support:

pip install pytest-fixture-cache[monad]

Quick Start

Basic Usage

import pytest
from pytest_fixture_cache import cached_fixture

@cached_fixture
@pytest.fixture
def expensive_fixture():
    """This will be cached after first execution"""
    # Expensive operation (API call, DB setup, etc.)
    import time
    time.sleep(5)  # Simulating slow operation
    return {"data": "value", "id": 123}

def test_something(expensive_fixture):
    # First run: 5 seconds (executes fixture)
    # Subsequent runs: <0.1 seconds (loads from cache)
    assert expensive_fixture["id"] == 123

CLI Usage

# Run tests with caching (default)
pytest

# Clear cache before running tests
pytest --clear-cache

# Disable caching for this run
pytest --no-cache

# Show cache statistics
pytest --cache-stats

Cache Statistics Output

================================================================================
FIXTURE CACHE STATISTICS
================================================================================

Found 3 cached fixture(s):

Fixture: expensive_api_fixture
  Scope:   session
  Status:  clean
  Hits:    42
  Created: 2025-01-25 10:30:15
  Updated: 2025-01-25 10:30:15

Fixture: database_fixture
  Scope:   module
  Status:  clean
  Hits:    15
  Created: 2025-01-25 10:31:20
  Updated: 2025-01-25 10:31:20

================================================================================

Advanced Usage

Programmatic Cache Management

from pytest_fixture_cache import (
    clear_fixture_cache,
    mark_fixture_dirty,
    get_cache_stats
)

# Clear specific fixture
clear_fixture_cache("my_fixture")

# Clear all fixtures
clear_fixture_cache()

# Mark fixture dirty (invalidate)
mark_fixture_dirty("my_fixture")

# Get statistics
stats = get_cache_stats()
for entry in stats:
    print(f"{entry['fixtureName']}: {entry['hitCount']} hits")

Mark Cache Dirty in Tests

def test_modify_data(expensive_fixture, mark_dirty):
    """Test that modifies fixture data"""
    # Use fixture
    data_id = expensive_fixture['id']

    # Modify the data (making cache invalid)
    delete_data(data_id)

    # Mark cache dirty so next test gets fresh data
    mark_dirty('expensive_fixture')

Result Monad Integration

For functional programming patterns using returns or result libraries:

from returns.result import Result, Success, Failure
from pytest_fixture_cache.monad import cached_result

def create_order():
    """Returns Result[OrderInfo, str]"""
    # ... API call that returns Result type
    return Success({"order_id": "123", "status": "created"})

@pytest.fixture
def order_fixture():
    """Cached fixture with Result monad"""
    result = cached_result("order_fixture", create_order)
    assert isinstance(result, Success)
    return result.unwrap()

Configuration

Set cache directory via environment variable:

# Use custom cache directory
export PYTEST_FIXTURE_CACHE_DIR="/path/to/cache"
pytest

# Default: .pytest_cache/fixtures/

In your tests:

from pytest_fixture_cache import get_cache_location

def test_cache_location():
    cache_path = get_cache_location()
    print(f"Cache stored at: {cache_path}")

How It Works

  1. First Run: Fixture executes normally, result is cached to SQLite
  2. Subsequent Runs: Cached data is loaded (skips expensive operations)
  3. Invalidation: Mark fixtures dirty when data changes
  4. Smart Loading: Only loads if status is "clean"
flowchart LR
    A[Test Runs] --> B{Cache Exists?}
    B -->|No| C[Execute Fixture]
    B -->|Yes| D{Status Clean?}
    D -->|Yes| E[Load from Cache]
    D -->|No| C
    C --> F[Save to Cache]
    F --> G[Return Result]
    E --> G

Cache Storage Schema

SQLite database: .pytest_cache/fixtures/cache.db

CREATE TABLE fixture_cache (
    id INTEGER PRIMARY KEY,
    fixtureName TEXT UNIQUE,
    fixtureScope TEXT,
    fixtureData TEXT,  -- JSON serialized
    status TEXT,       -- 'clean' or 'dirty'
    createdAt TEXT,
    updatedAt TEXT,
    hitCount INTEGER
)

Use Cases

1. API Integration Tests

@cached_fixture
@pytest.fixture(scope="session")
def api_auth_token():
    """Cache authentication token for entire test session"""
    response = requests.post("https://api.example.com/auth",
                            json={"user": "test", "pass": "secret"})
    return {"token": response.json()["access_token"]}

2. Database Seeding

@cached_fixture
@pytest.fixture(scope="module")
def seeded_database():
    """Cache database seed data for all tests in module"""
    db = Database()
    db.seed_test_data(1000_rows=True)  # Expensive!
    return {"db": db, "record_count": 1000}

3. File Generation

@cached_fixture
@pytest.fixture
def generated_report():
    """Cache generated PDF report"""
    report = ReportGenerator().create_pdf(complex_data=True)
    return {"path": report.path, "size": report.size}

Performance Impact

Example benchmark (100 tests, 3 expensive fixtures):

Scenario Without Cache With Cache Speedup
First run 45.2s 45.5s 1.0x (cache creation overhead)
Second run 45.1s 4.8s 9.4x faster
Third run 44.9s 4.7s 9.5x faster

Best Practices

  1. Cache Expensive Operations Only

    • Don't cache trivial fixtures (overhead > savings)
    • Best for: API calls, DB operations, file I/O
  2. Use Appropriate Scope

    • session: Data shared across all tests
    • module: Data shared within a module
    • function: Per-test data (least caching benefit)
  3. Invalidate When Needed

    • Mark dirty after tests that modify data
    • Clear cache when backend data changes
  4. Monitor Cache Effectiveness

    • Use --cache-stats to check hit counts
    • Low hits = fixture not reused enough to benefit

Comparison to Alternatives

Feature pytest-fixture-cache Built-in pytest.cache pytest-lazy-fixture
Automatic caching ✅ Decorator ❌ Manual ❌ No caching
Dirty/clean status ✅ Yes ❌ No N/A
Hit tracking ✅ Yes ❌ No N/A
CLI integration ✅ Yes ✅ Limited ❌ No
Result monad support ✅ Yes ❌ No ❌ No
Scope awareness ✅ Yes ❌ No ✅ Yes

Troubleshooting

Cache not working?

  1. Check fixture returns a dict:

    @cached_fixture
    @pytest.fixture
    def my_fixture():
        return {"key": "value"}  # ✅ Works
        # return "string"         # ❌ Won't cache
    
  2. Verify cache directory permissions:

    ls -la .pytest_cache/fixtures/
    
  3. Enable verbose output:

    pytest -v  # Shows "[CACHE HIT]" or "[CACHE MISS]" messages
    

Clear stuck cache:

pytest --clear-cache
# or
rm -rf .pytest_cache/fixtures/

Contributing

Contributions welcome! Please check the issues page.

Development Setup

git clone https://github.com/yourusername/pytest-fixture-cache.git
cd pytest-fixture-cache
pip install -e ".[dev]"
pytest tests/

License

MIT License - see LICENSE file for details.

Changelog

See CHANGELOG.md for version history.

Credits

Developed for the WMS testing framework, extracted as a standalone library to benefit the pytest community.

Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_fixture_cache-0.1.0.tar.gz (25.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_fixture_cache-0.1.0-py3-none-any.whl (14.6 kB view details)

Uploaded Python 3

File details

Details for the file pytest_fixture_cache-0.1.0.tar.gz.

File metadata

  • Download URL: pytest_fixture_cache-0.1.0.tar.gz
  • Upload date:
  • Size: 25.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for pytest_fixture_cache-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c0b3c2bb64f1a5d7a33392d493f45b9095d2f0a3348343b548622f92cf84a674
MD5 01efa4e827afbb48ae5129da40d8d1d5
BLAKE2b-256 e5cd34f04cc04c610324f3b06e71ffb57a238dc4644309182eb9184706a03299

See more details on using hashes here.

File details

Details for the file pytest_fixture_cache-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_fixture_cache-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6d997e4fd1b37da29973cd3060795c035cfd9cb4ab53fc35b66bb1b56bed3696
MD5 816f7c9db638ca027227478fbd546803
BLAKE2b-256 b1bf354b9c9f447953edd751da47f67bc8ae2f3041d2ddd51c882ab2970159be

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page