Testing¶
This guide covers test organization, pytest fixtures, and CI configuration for zae-limiter.
Test Organization¶
Tests are organized by execution environment and scope:
tests/
├── conftest.py # Shared config (--run-aws flag)
├── unit/ # Fast tests with mocked AWS (moto)
│ ├── test_limiter.py
│ ├── test_repository.py
│ └── test_sync_limiter.py
├── integration/ # LocalStack tests (repository-level)
│ └── test_repository.py
├── e2e/ # Full workflow tests (LocalStack + AWS)
│ ├── test_localstack.py
│ └── test_aws.py
└── benchmark/ # Performance benchmarks (pytest-benchmark)
├── test_operations.py # Mocked benchmarks
└── test_localstack.py # LocalStack benchmarks
Test Categories¶
| Category | Directory | Backend | What to Test | Speed |
|---|---|---|---|---|
| Unit | tests/unit/ |
moto (mocked) | Business logic, bucket math, schema, exceptions | Fast (~seconds) |
| Integration | tests/integration/ |
LocalStack | Repository operations, transactions, GSI queries | Medium |
| E2E | tests/e2e/ |
LocalStack or AWS | Full workflows: CLI, rate limiting, hierarchical limits | Slow |
| Benchmark | tests/benchmark/ |
moto or LocalStack | Latency (p50/p95/p99), throughput, cascade overhead | Variable |
Pytest Markers¶
| Marker | Description | How to Run |
|---|---|---|
| (none) | Unit tests | pytest tests/unit/ |
@pytest.mark.integration |
Requires LocalStack | pytest -m integration |
@pytest.mark.e2e |
End-to-end workflows | pytest -m e2e |
@pytest.mark.aws |
Real AWS (requires --run-aws) |
pytest -m aws --run-aws |
@pytest.mark.benchmark |
Performance benchmarks | pytest -m benchmark |
@pytest.mark.slow |
Tests with >30s waits | Skip with -m "not slow" |
pytest Fixtures¶
LocalStack Endpoint Fixture¶
import os
import pytest
@pytest.fixture
def localstack_endpoint():
"""Get LocalStack endpoint from environment."""
return os.getenv("AWS_ENDPOINT_URL", "http://localhost:4566")
Function-Scoped Limiter (Isolated)¶
import uuid
import pytest
from zae_limiter import RateLimiter, StackOptions
@pytest.fixture(scope="function")
async def limiter(localstack_endpoint):
"""
Create a rate limiter connected to LocalStack with automatic cleanup.
This fixture:
1. Creates a unique stack for test isolation
2. Yields the limiter for test use
3. Deletes the stack in teardown
"""
# Unique name prevents test interference
name = f"test-{uuid.uuid4().hex[:8]}"
limiter = RateLimiter(
name=name, # Creates ZAEL-test-{uuid} resources
endpoint_url=localstack_endpoint,
region="us-east-1",
stack_options=StackOptions(enable_aggregator=False),
)
async with limiter:
yield limiter
# Cleanup: delete the CloudFormation stack
await limiter.delete_stack()
@pytest.mark.integration
async def test_rate_limiting(limiter):
async with limiter.acquire(
entity_id="test-user",
resource="api",
limits=[Limit.per_minute("requests", 10)],
consume={"requests": 1},
):
pass # Success
Session-Scoped Limiter (Faster)¶
For test suites where stack creation overhead is significant:
@pytest.fixture(scope="session")
async def shared_limiter(localstack_endpoint):
"""
Session-scoped limiter for faster test execution.
Trade-off: Tests share state, less isolation.
"""
limiter = RateLimiter(
name="integration-test-shared",
endpoint_url=localstack_endpoint,
region="us-east-1",
stack_options=StackOptions(enable_aggregator=False),
)
async with limiter:
yield limiter
await limiter.delete_stack()
Sync Fixture Example¶
@pytest.fixture(scope="function")
def sync_limiter(localstack_endpoint):
"""Synchronous rate limiter with cleanup."""
from zae_limiter import SyncRateLimiter, StackOptions
import uuid
name = f"test-sync-{uuid.uuid4().hex[:8]}"
limiter = SyncRateLimiter(
name=name,
endpoint_url=localstack_endpoint,
region="us-east-1",
stack_options=StackOptions(enable_aggregator=False),
)
with limiter:
yield limiter
limiter.delete_stack()
Running Tests¶
Unit Tests Only (No Docker)¶
Integration Tests (Requires LocalStack)¶
# Start LocalStack
docker compose up -d
# Set environment variables
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
# Run integration tests
pytest tests/integration/ -v
# Stop LocalStack
docker compose down
E2E Tests¶
# LocalStack E2E
pytest tests/e2e/test_localstack.py -v
# Real AWS E2E (costs money!)
pytest tests/e2e/test_aws.py --run-aws -v
Benchmarks¶
# Mocked benchmarks (fast)
pytest tests/benchmark/test_operations.py -v
# LocalStack benchmarks (realistic latency)
pytest tests/benchmark/test_localstack.py -v
# Export results to JSON
pytest tests/benchmark/ -v --benchmark-json=benchmark.json
CI Configuration¶
Example GitHub Actions workflow for integration tests:
# .github/workflows/ci.yml
jobs:
integration:
runs-on: ubuntu-latest
services:
localstack:
image: localstack/localstack
ports:
- 4566:4566
env:
SERVICES: dynamodb,dynamodbstreams,lambda,cloudformation,logs,iam,cloudwatch,sqs
options: >-
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock
steps:
- uses: actions/checkout@v4
- run: pip install -e ".[dev]"
- run: pytest -m integration
env:
AWS_ENDPOINT_URL: http://localhost:4566
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
When to Add Tests¶
- New business logic (bucket calculations, limit validation) →
unit/ - New DynamoDB operations (queries, transactions, GSI) →
integration/ - New user-facing features (CLI commands, rate limiting workflows) →
e2e/ - AWS-specific behavior (alarms, DLQ, CloudWatch metrics) →
e2e/test_aws.py - Performance-sensitive code (new operations, optimizations) →
benchmark/
Test Coverage¶
Next Steps¶
- LocalStack - Local AWS development environment
- Architecture - Understanding the codebase