CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-hishel

Persistent cache implementation for httpx and httpcore following RFC 9111 specification

74

1.48x
Overview
Eval results
Files

testing-utilities.mddocs/

Testing Utilities

Mock connection pools and transports for unit testing HTTP caching behavior. These utilities allow you to test caching logic without making actual network requests.

Capabilities

Mock Synchronous Transport

Mock transport for testing synchronous HTTP client caching behavior.

class MockTransport(httpx.BaseTransport):
    def handle_request(self, request: httpx.Request) -> httpx.Response:
        """Handle request by returning pre-configured mock response"""
    
    def add_responses(self, responses: list[httpx.Response]) -> None:
        """
        Add mock responses to be returned in order.
        
        Parameters:
        - responses: List of httpx.Response objects to return
        """

Usage Examples:

import httpx
import hishel

# Create mock responses
response1 = httpx.Response(200, json={"data": "first"})
response2 = httpx.Response(200, json={"data": "second"})

# Set up mock transport
mock_transport = hishel.MockTransport()
mock_transport.add_responses([response1, response2])

# Use with cache client for testing
cache_transport = hishel.CacheTransport(transport=mock_transport)
with httpx.Client(transport=cache_transport) as client:
    # First request uses first mock response
    resp1 = client.get("https://api.example.com/data")
    assert resp1.json() == {"data": "first"}
    
    # Second request should be served from cache
    resp2 = client.get("https://api.example.com/data")  
    assert resp2.json() == {"data": "first"}  # Same as first (cached)

Mock Asynchronous Transport

Mock transport for testing asynchronous HTTP client caching behavior.

class MockAsyncTransport(httpx.AsyncBaseTransport):
    async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
        """Handle async request by returning pre-configured mock response"""
    
    def add_responses(self, responses: list[httpx.Response]) -> None:
        """
        Add mock responses to be returned in order.
        
        Parameters:
        - responses: List of httpx.Response objects to return
        """

Usage Examples:

import httpx
import hishel
import asyncio

async def test_async_caching():
    # Create mock responses with cache headers
    response = httpx.Response(
        200,
        headers={"cache-control": "max-age=3600"},
        json={"data": "cached"}
    )
    
    # Set up mock async transport
    mock_transport = hishel.MockAsyncTransport()
    mock_transport.add_responses([response])
    
    # Use with async cache client
    cache_transport = hishel.AsyncCacheTransport(transport=mock_transport)
    async with httpx.AsyncClient(transport=cache_transport) as client:
        # First request uses mock response
        resp1 = await client.get("https://api.example.com/data")
        assert resp1.json() == {"data": "cached"}
        
        # Second request served from cache (no additional mock response needed)
        resp2 = await client.get("https://api.example.com/data")
        assert resp2.json() == {"data": "cached"}

asyncio.run(test_async_caching())

Mock Connection Pools

Lower-level mock connection pools for testing httpcore-level caching.

class MockConnectionPool:
    def handle_request(self, request: httpcore.Request) -> httpcore.Response:
        """Handle httpcore request with mock response"""
    
    def add_responses(self, responses: list[httpcore.Response]) -> None:
        """
        Add mock httpcore responses.
        
        Parameters:
        - responses: List of httpcore.Response objects
        """

class MockAsyncConnectionPool:
    async def handle_async_request(self, request: httpcore.Request) -> httpcore.Response:
        """Handle async httpcore request with mock response"""
    
    def add_responses(self, responses: list[httpcore.Response]) -> None:
        """
        Add mock httpcore responses.
        
        Parameters:
        - responses: List of httpcore.Response objects
        """

Usage Examples:

import httpcore
import hishel

# Create mock httpcore responses
response = httpcore.Response(
    200,
    headers=[(b"content-type", b"application/json")],
    content=b'{"data": "test"}'
)

# Test with mock connection pool
mock_pool = hishel.MockConnectionPool()
mock_pool.add_responses([response])

# Use for lower-level testing
request = httpcore.Request(b"GET", b"https://api.example.com/data")
response = mock_pool.handle_request(request)
assert response.status == 200

Testing Cache Behavior

Test Cache Hit/Miss

import httpx
import hishel
import pytest

def test_cache_hit_miss():
    # Mock response with cache headers
    cached_response = httpx.Response(
        200,
        headers={"cache-control": "max-age=3600", "etag": '"abc123"'},
        json={"data": "original"}
    )
    
    # Fresh response for revalidation
    fresh_response = httpx.Response(
        200,
        headers={"cache-control": "max-age=3600", "etag": '"def456"'},
        json={"data": "updated"}
    )
    
    mock_transport = hishel.MockTransport()
    mock_transport.add_responses([cached_response, fresh_response])
    
    storage = hishel.InMemoryStorage()
    cache_transport = hishel.CacheTransport(
        transport=mock_transport,
        storage=storage
    )
    
    with httpx.Client(transport=cache_transport) as client:
        # First request - cache miss
        resp1 = client.get("https://api.example.com/data")
        assert resp1.json() == {"data": "original"}
        
        # Second request - cache hit
        resp2 = client.get("https://api.example.com/data")
        assert resp2.json() == {"data": "original"}  # Served from cache
        
        # Verify only one network request was made
        # (second response in mock is unused)

Test Cache Revalidation

import httpx
import hishel

def test_cache_revalidation():
    # Original response
    original_response = httpx.Response(
        200,
        headers={"cache-control": "max-age=0", "etag": '"version1"'},
        json={"version": 1}
    )
    
    # 304 Not Modified response
    not_modified_response = httpx.Response(
        304,
        headers={"cache-control": "max-age=3600", "etag": '"version1"'}
    )
    
    mock_transport = hishel.MockTransport()
    mock_transport.add_responses([original_response, not_modified_response])
    
    cache_transport = hishel.CacheTransport(transport=mock_transport)
    
    with httpx.Client(transport=cache_transport) as client:
        # First request
        resp1 = client.get("https://api.example.com/data")
        assert resp1.json() == {"version": 1}
        
        # Second request triggers revalidation due to max-age=0
        resp2 = client.get("https://api.example.com/data")
        assert resp2.json() == {"version": 1}  # Same content (304 response)
        assert resp2.status_code == 200       # But status is 200 (from cache)

Test Different Storage Backends

import httpx
import hishel
import tempfile
import pytest

@pytest.mark.parametrize("storage_class", [
    hishel.InMemoryStorage,
    hishel.FileStorage,
])
def test_storage_backends(storage_class):
    response = httpx.Response(
        200,
        headers={"cache-control": "max-age=3600"},
        json={"backend": storage_class.__name__}
    )
    
    mock_transport = hishel.MockTransport()
    mock_transport.add_responses([response])
    
    # Configure storage based on type
    if storage_class == hishel.FileStorage:
        with tempfile.TemporaryDirectory() as tmpdir:
            storage = storage_class(base_path=tmpdir)
    else:
        storage = storage_class()
    
    cache_transport = hishel.CacheTransport(
        transport=mock_transport,
        storage=storage
    )
    
    with httpx.Client(transport=cache_transport) as client:
        resp1 = client.get("https://api.example.com/data")
        resp2 = client.get("https://api.example.com/data")  # From cache
        
        assert resp1.json() == resp2.json()

Test Cache Control Directives

import httpx
import hishel

def test_no_cache_directive():
    # Response with no-cache directive
    response = httpx.Response(
        200,
        headers={"cache-control": "no-cache"},
        json={"directive": "no-cache"}
    )
    
    revalidation_response = httpx.Response(
        200,
        headers={"cache-control": "max-age=3600"},
        json={"directive": "revalidated"}
    )
    
    mock_transport = hishel.MockTransport()
    mock_transport.add_responses([response, revalidation_response])
    
    cache_transport = hishel.CacheTransport(transport=mock_transport)
    
    with httpx.Client(transport=cache_transport) as client:
        # First request
        resp1 = client.get("https://api.example.com/data")
        assert resp1.json() == {"directive": "no-cache"}
        
        # Second request should revalidate due to no-cache
        resp2 = client.get("https://api.example.com/data")
        assert resp2.json() == {"directive": "revalidated"}

Test Utilities

Custom Test Storage

import hishel

class TestStorage(hishel.BaseStorage):
    """Test storage that tracks operations"""
    
    def __init__(self):
        super().__init__()
        self.stored_keys = []
        self.retrieved_keys = []
        self.cache = {}
    
    def store(self, key, response, request, metadata=None):
        self.stored_keys.append(key)
        self.cache[key] = (response, request, metadata)
    
    def retrieve(self, key):
        self.retrieved_keys.append(key)
        return self.cache.get(key)
    
    def remove(self, key):
        self.cache.pop(key, None)
    
    def update_metadata(self, key, response, request, metadata):
        if key in self.cache:
            self.cache[key] = (response, request, metadata)
    
    def close(self):
        pass

# Use in tests
def test_with_custom_storage():
    test_storage = TestStorage()
    
    response = httpx.Response(200, json={"test": True})
    mock_transport = hishel.MockTransport()
    mock_transport.add_responses([response])
    
    cache_transport = hishel.CacheTransport(
        transport=mock_transport,
        storage=test_storage
    )
    
    with httpx.Client(transport=cache_transport) as client:
        client.get("https://api.example.com/data")
        client.get("https://api.example.com/data")  # Cache hit
    
    assert len(test_storage.stored_keys) == 1
    assert len(test_storage.retrieved_keys) == 1

Install with Tessl CLI

npx tessl i tessl/pypi-hishel

docs

cache-clients.md

cache-controller.md

cache-transports.md

http-headers.md

index.md

serializers.md

storage-backends.md

testing-utilities.md

tile.json