Development Testing¶
Comprehensive guide to testing practices and infrastructure for UUID-Forge development.
Testing Philosophy¶
UUID-Forge follows a comprehensive testing strategy that ensures:
- Correctness: All functionality works as specified
- Determinism: UUIDs are consistently generated
- Performance: Generation is fast and efficient
- Reliability: Code works across different environments
- Maintainability: Tests are clear and maintainable
Test Structure¶
Test Organization¶
tests/
├── conftest.py # Shared test configuration
├── test_core.py # Core functionality tests
├── test_config.py # Configuration tests
├── test_cli.py # CLI tests
├── test_init.py # Package initialization tests
├── test_version.py # Version tests
├── integration/ # Integration tests
│ ├── test_database.py # Database integration
│ ├── test_api.py # API integration
│ └── test_cli_integration.py # CLI integration
├── performance/ # Performance tests
│ ├── test_benchmarks.py # Benchmark tests
│ └── test_memory.py # Memory usage tests
└── property/ # Property-based tests
├── test_determinism.py # Determinism properties
└── test_uniqueness.py # Uniqueness properties
Test Categories¶
Unit Tests: Test individual functions and classes in isolation Integration Tests: Test component interactions and workflows Property Tests: Test mathematical properties using generated data Performance Tests: Benchmark and validate performance requirements Regression Tests: Prevent previously fixed bugs from reoccurring
Unit Testing¶
Core Functionality Tests¶
import pytest
import uuid
from uuid_forge.core import UUIDGenerator, IDConfig
class TestUUIDGenerator:
"""Test core UUID generation functionality."""
def setUp(self):
self.config = IDConfig(namespace="test-namespace")
self.generator = UUIDGenerator(self.config)
def test_deterministic_generation(self):
"""Test that same input produces same UUID."""
input_data = "test-input"
uuid1 = self.generator.generate(input_data)
uuid2 = self.generator.generate(input_data)
assert uuid1 == uuid2
assert isinstance(uuid1, str)
assert len(uuid1) == 36
def test_different_inputs_different_uuids(self):
"""Test that different inputs produce different UUIDs."""
uuid1 = self.generator.generate("input1")
uuid2 = self.generator.generate("input2")
assert uuid1 != uuid2
def test_valid_uuid_format(self):
"""Test that generated UUIDs have valid format."""
uuid_result = self.generator.generate("test")
# Should be parseable as UUID
parsed_uuid = uuid.UUID(uuid_result)
assert str(parsed_uuid) == uuid_result
# Should have correct format
assert len(uuid_result) == 36
assert uuid_result.count('-') == 4
@pytest.mark.parametrize("input_data", [
"string",
{"key": "value"},
["list", "item"],
42,
3.14,
True,
None
])
def test_various_input_types(self, input_data):
"""Test UUID generation with various input types."""
uuid_result = self.generator.generate(input_data)
assert isinstance(uuid_result, str)
assert len(uuid_result) == 36
# Same input should produce same UUID
uuid_again = self.generator.generate(input_data)
assert uuid_result == uuid_again
def test_namespace_isolation(self):
"""Test that different namespaces produce different UUIDs."""
config1 = IDConfig(namespace="namespace1")
config2 = IDConfig(namespace="namespace2")
gen1 = UUIDGenerator(config1)
gen2 = UUIDGenerator(config2)
input_data = "same-input"
uuid1 = gen1.generate(input_data)
uuid2 = gen2.generate(input_data)
assert uuid1 != uuid2
def test_empty_input_handling(self):
"""Test handling of empty inputs."""
empty_inputs = ["", {}, [], None]
for empty_input in empty_inputs:
uuid_result = self.generator.generate(empty_input)
assert isinstance(uuid_result, str)
assert len(uuid_result) == 36
Configuration Tests¶
from uuid_forge.config import load_config_from_env, init_config_file
from uuid_forge.core import IDConfig
import os
import tempfile
class TestConfiguration:
"""Test configuration loading and validation."""
def test_default_config(self):
"""Test default configuration values."""
config = IDConfig()
assert config.namespace is not None
assert isinstance(config.salt, str)
assert len(config.salt) > 0
def test_custom_namespace(self):
"""Test custom namespace configuration."""
custom_namespace = "custom-test-namespace"
config = IDConfig(namespace=custom_namespace)
assert config.namespace == custom_namespace
def test_environment_config_loading(self):
"""Test loading configuration from environment variables."""
test_namespace = "env-test-namespace"
test_salt = "env-test-salt"
# Set environment variables
os.environ["UUID_FORGE_NAMESPACE"] = test_namespace
os.environ["UUID_FORGE_SALT"] = test_salt
try:
config = load_config_from_env()
assert config.namespace == test_namespace
assert config.salt == test_salt
finally:
# Cleanup
del os.environ["UUID_FORGE_NAMESPACE"]
del os.environ["UUID_FORGE_SALT"]
def test_config_file_creation(self):
"""Test configuration file creation."""
with tempfile.TemporaryDirectory() as temp_dir:
config_path = os.path.join(temp_dir, "test_config.yaml")
init_config_file(config_path)
assert os.path.exists(config_path)
# File should contain expected content
with open(config_path, 'r') as f:
content = f.read()
assert "namespace:" in content
assert "salt:" in content
CLI Tests¶
from typer.testing import CliRunner
from uuid_forge.cli import app
import json
class TestCLI:
"""Test command-line interface."""
def setUp(self):
self.runner = CliRunner()
def test_generate_command(self):
"""Test basic UUID generation command."""
result = self.runner.invoke(app, ["generate", "test-input"])
assert result.exit_code == 0
output = result.stdout.strip()
assert len(output) == 36
assert output.count('-') == 4
def test_generate_multiple_inputs(self):
"""Test generating UUIDs for multiple inputs."""
result = self.runner.invoke(app, [
"generate", "input1", "input2", "input3"
])
assert result.exit_code == 0
lines = result.stdout.strip().split('\n')
assert len(lines) == 3
# All should be valid UUIDs
for line in lines:
assert len(line) == 36
assert line.count('-') == 4
# All should be different
assert len(set(lines)) == 3
def test_namespace_option(self):
"""Test namespace option."""
result1 = self.runner.invoke(app, [
"generate", "--namespace", "ns1", "test"
])
result2 = self.runner.invoke(app, [
"generate", "--namespace", "ns2", "test"
])
assert result1.exit_code == 0
assert result2.exit_code == 0
uuid1 = result1.stdout.strip()
uuid2 = result2.stdout.strip()
# Different namespaces should produce different UUIDs
assert uuid1 != uuid2
def test_config_commands(self):
"""Test configuration management commands."""
# Test config show
result = self.runner.invoke(app, ["config", "show"])
assert result.exit_code == 0
# Output should contain configuration information
assert "namespace" in result.stdout.lower()
def test_version_command(self):
"""Test version command."""
result = self.runner.invoke(app, ["--version"])
assert result.exit_code == 0
assert "uuid-forge" in result.stdout.lower()
def test_help_commands(self):
"""Test help commands."""
result = self.runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "generate" in result.stdout
result = self.runner.invoke(app, ["generate", "--help"])
assert result.exit_code == 0
assert "namespace" in result.stdout
Integration Testing¶
Database Integration¶
import pytest
import sqlite3
from uuid_forge import UUIDGenerator
class TestDatabaseIntegration:
"""Test integration with database systems."""
@pytest.fixture
def test_db(self):
"""Create test database."""
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL
)
""")
cursor.execute("""
CREATE TABLE orders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
total REAL NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
)
""")
conn.commit()
yield conn
conn.close()
def test_user_order_relationship(self, test_db):
"""Test maintaining relationships with deterministic UUIDs."""
user_gen = UUIDGenerator(IDConfig(namespace=Namespace("db-users"), salt="v1"))
order_gen = UUIDGenerator(IDConfig(namespace=Namespace("db-orders"), salt="v1"))
cursor = test_db.cursor()
# Create user with deterministic UUID
user_email = "dbtest@example.com"
user_id = user_gen.generate("user", email=user_email)
cursor.execute(
"INSERT INTO users (id, email, name) VALUES (?, ?, ?)",
(user_id, user_email, "Test User")
)
# Create order with deterministic UUID
order_data = {
"user_id": user_id,
"total": 100.50,
"items": ["item1", "item2"]
}
order_id = order_gen.generate(order_data)
cursor.execute(
"INSERT INTO orders (id, user_id, total) VALUES (?, ?, ?)",
(order_id, user_id, 100.50)
)
test_db.commit()
# Verify relationship
cursor.execute("""
SELECT u.email, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.id = ?
""", (user_id,))
result = cursor.fetchone()
assert result is not None
assert result[0] == user_email
assert result[1] == 100.50
# Verify UUIDs are deterministic
user_id_2 = user_gen.generate("user", email=user_email)
order_id_2 = order_gen.generate(order_data)
assert user_id == user_id_2
assert order_id == order_id_2
API Integration¶
import pytest
import requests_mock
from uuid_forge import UUIDGenerator
class TestAPIIntegration:
"""Test integration with API services."""
def test_rest_api_integration(self):
"""Test integration with REST API."""
user_gen = UUIDGenerator(IDConfig(namespace=Namespace("api-users"), salt="v1"))
with requests_mock.Mocker() as m:
user_email = "apitest@example.com"
user_id = user_gen.generate("user", email=user_email)
# Mock API response
m.post(
"http://api.example.com/users",
json={"id": user_id, "email": user_email},
status_code=201
)
# Test API call
response = requests.post(
"http://api.example.com/users",
json={"email": user_email}
)
assert response.status_code == 201
data = response.json()
assert data["id"] == user_id
assert data["email"] == user_email
Property-Based Testing¶
Determinism Properties¶
from hypothesis import given, strategies as st
from uuid_forge import UUIDGenerator
import uuid
class TestDeterminismProperties:
"""Test determinism properties using Hypothesis."""
def setUp(self):
self.generator = UUIDGenerator(IDConfig(namespace=Namespace("property-test"), salt="v1"))
@given(st.text(min_size=1))
def test_determinism_property(self, input_text):
"""Property: Same input always produces same output."""
uuid1 = self.generator.generate(input_text)
uuid2 = self.generator.generate(input_text)
assert uuid1 == uuid2
@given(st.text(min_size=1))
def test_valid_uuid_property(self, input_text):
"""Property: All outputs are valid UUIDs."""
uuid_result = self.generator.generate(input_text)
# Should be parseable as UUID
parsed = uuid.UUID(uuid_result)
assert str(parsed) == uuid_result
# Should have correct length and format
assert len(uuid_result) == 36
assert uuid_result.count('-') == 4
@given(st.lists(st.text(min_size=1), min_size=2, unique=True))
def test_uniqueness_property(self, input_list):
"""Property: Different inputs produce different UUIDs."""
uuids = [self.generator.generate(inp) for inp in input_list]
# All UUIDs should be unique
assert len(set(uuids)) == len(uuids)
@given(st.dictionaries(st.text(), st.text(), min_size=1))
def test_dict_determinism_property(self, input_dict):
"""Property: Dictionary inputs produce deterministic UUIDs."""
uuid1 = self.generator.generate(input_dict)
uuid2 = self.generator.generate(input_dict)
assert uuid1 == uuid2
Performance Testing¶
Benchmark Tests¶
import pytest
from uuid_forge import UUIDGenerator
import time
class TestPerformance:
"""Test performance requirements."""
def setUp(self):
self.generator = UUIDGenerator(IDConfig(namespace=Namespace("perf-test"), salt="v1"))
@pytest.mark.benchmark
def test_single_generation_speed(self, benchmark):
"""Benchmark single UUID generation."""
result = benchmark(self.generator.generate, "benchmark-input")
assert len(result) == 36
def test_batch_generation_performance(self):
"""Test batch generation performance."""
test_inputs = [f"input-{i}" for i in range(1000)]
start_time = time.time()
uuids = [self.generator.generate(inp) for inp in test_inputs]
end_time = time.time()
generation_time = end_time - start_time
# Should generate 1000 UUIDs in under 100ms
assert generation_time < 0.1
assert len(uuids) == 1000
assert len(set(uuids)) == 1000 # All unique
def test_memory_efficiency(self):
"""Test memory usage during generation."""
import psutil
import os
process = psutil.Process(os.getpid())
initial_memory = process.memory_info().rss
# Generate many UUIDs
large_inputs = [f"input-{i}" for i in range(10000)]
uuids = [self.generator.generate(inp) for inp in large_inputs]
peak_memory = process.memory_info().rss
memory_increase = peak_memory - initial_memory
# Memory increase should be reasonable
assert memory_increase < 50 * 1024 * 1024 # Less than 50MB
assert len(uuids) == 10000
@pytest.mark.slow
def test_large_scale_performance(self):
"""Test performance with large-scale generation."""
# Generate 100,000 UUIDs
start_time = time.time()
uuids = []
for i in range(100000):
uuid_result = self.generator.generate(f"large-scale-{i}")
uuids.append(uuid_result)
end_time = time.time()
total_time = end_time - start_time
# Should complete in reasonable time
assert total_time < 10.0 # Less than 10 seconds
assert len(uuids) == 100000
assert len(set(uuids)) == 100000 # All unique
Memory Profiling¶
import pytest
from memory_profiler import profile
from uuid_forge import UUIDGenerator
class TestMemoryUsage:
"""Test memory usage patterns."""
@profile
def test_memory_profile_batch_generation(self):
"""Profile memory usage during batch generation."""
generator = UUIDGenerator(IDConfig(namespace=Namespace("memory-test"), salt="v1"))
# Generate many UUIDs to observe memory pattern
uuids = []
for i in range(10000):
uuid_result = generator.generate(f"memory-test-{i}")
uuids.append(uuid_result)
return len(uuids)
Test Configuration and Fixtures¶
Shared Test Configuration¶
# conftest.py
import pytest
from uuid_forge import UUIDGenerator
from uuid_forge.core import IDConfig
import tempfile
import os
@pytest.fixture
def test_generator():
"""Provide a test UUID generator with consistent namespace."""
config = IDConfig(namespace="test-namespace")
return UUIDGenerator(config)
@pytest.fixture
def temp_config_file():
"""Provide a temporary configuration file."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write("""
namespace: test-config-namespace
salt: test-config-salt
""")
config_path = f.name
yield config_path
# Cleanup
os.unlink(config_path)
@pytest.fixture
def sample_test_data():
"""Provide sample test data."""
return {
"users": [
{"email": "user1@test.com", "name": "User One"},
{"email": "user2@test.com", "name": "User Two"},
{"email": "user3@test.com", "name": "User Three"}
],
"orders": [
{"id": "order1", "user_email": "user1@test.com", "total": 100.0},
{"id": "order2", "user_email": "user2@test.com", "total": 200.0}
]
}
@pytest.fixture(scope="session")
def performance_generator():
"""Provide a generator for performance tests."""
config = IDConfig(namespace="performance-test")
return UUIDGenerator(config)
# Test markers
pytest.mark.slow = pytest.mark.mark_slow("Slow running tests")
pytest.mark.benchmark = pytest.mark.benchmark("Benchmark tests")
pytest.mark.integration = pytest.mark.integration("Integration tests")
Test Execution¶
Running Tests¶
# Run all tests
uv run pytest
# Run specific test categories
uv run pytest -m "not slow" # Skip slow tests
uv run pytest -m benchmark # Only benchmark tests
uv run pytest -m integration # Only integration tests
# Run with coverage
uv run pytest --cov=uuid_forge --cov-report=html
# Run specific test file
uv run pytest tests/test_core.py
# Run specific test
uv run pytest tests/test_core.py::TestUUIDGenerator::test_deterministic_generation
# Run tests matching pattern
uv run pytest -k "test_uuid"
# Run tests with verbose output
uv run pytest -v
# Run tests with detailed output
uv run pytest -vv
# Stop on first failure
uv run pytest -x
# Run failed tests from last run
uv run pytest --lf
Continuous Integration¶
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: |
pip install uv
- name: Install dependencies
run: |
uv sync --dev
- name: Run tests
run: |
uv run pytest --cov=uuid_forge --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Test Best Practices¶
Writing Good Tests¶
- Clear Names: Test names should describe what is being tested
- Single Responsibility: Each test should test one thing
- Deterministic: Tests should not depend on external factors
- Fast: Unit tests should run quickly
- Independent: Tests should not depend on each other
Test Organization¶
- Group Related Tests: Use classes to group related test methods
- Use Fixtures: Share setup code using pytest fixtures
- Parametrize Tests: Use
@pytest.mark.parametrizefor similar tests - Mark Tests: Use pytest marks to categorize tests
Coverage Goals¶
- Unit Tests: Aim for >95% code coverage
- Integration Tests: Cover critical workflows
- Property Tests: Validate mathematical properties
- Performance Tests: Ensure performance requirements
Debugging Tests¶
Debug Failing Tests¶
# Run with pdb debugger
uv run pytest --pdb
# Run specific failing test with verbose output
uv run pytest tests/test_core.py::test_failing -vv
# Show local variables in traceback
uv run pytest --tb=long
Test Output Analysis¶
# Show print statements
uv run pytest -s
# Show test duration
uv run pytest --durations=10
# Generate HTML coverage report
uv run pytest --cov=uuid_forge --cov-report=html
open htmlcov/index.html
Next Steps¶
- Release Process - Preparing releases
- Contributing - Contributing guidelines
- Best Practices - Code best practices