Skip to content

๐Ÿ“‹ Test Writing Guide for loryth-common

This guide provides comprehensive standards and best practices for writing high-quality tests in the loryth-common library, based on insights from technical designs TD-006, TD-007, and lessons learned from implementing comprehensive test coverage.

๐ŸŽฏ Test Categories and Organization

Test Structure Overview

tests/
โ”œโ”€โ”€ unit/                  # Isolated component tests (fast, focused)
โ”œโ”€โ”€ integration/           # Cross-component workflow tests (realistic scenarios)
โ”œโ”€โ”€ regression/            # Bug prevention and edge case tests (specific issues)
โ”œโ”€โ”€ resources/             # Test data packages and sample modules
โ””โ”€โ”€ test_helpers/          # Test utilities and standards

Category Guidelines

Unit Tests (tests/unit/)

  • Purpose: Test individual components in isolation
  • Characteristics: Fast execution (<100ms), no external dependencies, focused scope
  • Mocking: Mock all dependencies except the component under test
  • Examples: Cache operations, single class methods, utility functions

Integration Tests (tests/integration/)

  • Purpose: Test component interactions and end-to-end workflows
  • Characteristics: Realistic scenarios, multiple components working together
  • Dependencies: Real implementations of collaborating components
  • Examples: Complete reflection workflows, context API usage, pipeline operations

Regression Tests (tests/regression/)

  • Purpose: Prevent specific bugs and validate edge cases
  • Characteristics: Targeted at known failure modes, often complex scenarios
  • Documentation: Must reference the original issue or behavior being prevented
  • Examples: Forward reference resolution bugs, property precedence rules

๐Ÿ”ง Test Quality Standards

Core Testing Principles

  1. Avoid Mocking Unless Testing Integration Points: Mock only when testing specific seams/integration points, prefer real implementations
  2. Function-Based Tests Over Classes: Use standalone test functions with pytest fixtures, avoid test classes
  3. Metrics for Behavior Verification: Use metrics to verify "how many times" operations occur, not timing data
  4. Focus and Clarity: Each test should verify one specific behavior - if unclear what's being tested, rethink the test
  5. No Mocks in Integration Tests: Integration tests should use real components to test actual interactions

Anti-Patterns to Avoid

โŒ Try-Except with Pass (Masks Issues)

# BAD: Hides potential failures
try:
    result = operation()
    assert result is None
except Exception:
    pass  # Either outcome acceptable - WRONG!

โœ… Clear Expectations

# GOOD: Explicit about expected behavior
def test_operation_returns_none_for_invalid_input():
    """Test that operation returns None for invalid input."""
    result = operation(invalid_input)
    assert result is None

# OR if behavior should raise
def test_operation_raises_for_invalid_input():
    """Test that operation raises ValueError for invalid input."""
    with pytest.raises(ValueError, match="Invalid input"):
        operation(invalid_input)

โŒ Manual Exception Flag Tracking

# BAD: Verbose and error-prone
raised = False
try:
    operation_that_should_fail()
except SpecificException:
    raised = True
assert raised is True

โœ… pytest.raises() Usage

# GOOD: Built-in pytest capabilities
def test_operation_raises_specific_exception():
    """Test that operation raises SpecificException for invalid state."""
    with pytest.raises(SpecificException, match="expected error pattern"):
        operation_that_should_fail()

โŒ Verbose Boolean Assertions

# BAD: Unnecessary verbosity
assert condition is True
assert condition is False
assert value != None

โœ… Clean Assertions

# GOOD: Direct and clear
assert condition
assert not condition
assert value is not None

Assertion Best Practices

Specific and Clear Assertions

# GOOD: Exact comparisons with clear expectations
assert len(results) == 3  # Not > 0, be specific about expected count
assert result.status == "completed"
assert isinstance(result, ExpectedType)

# GOOD: Metrics-based behavior verification
test_metrics.assert_operation_count("type_hints.resolved", 1)  # Exactly once
success_events = test_metrics.get_events_for_operation("type_hints.resolved")
assert len(success_events) == 1  # Specific count, not > 0

# USE ASSERTION MESSAGES SPARINGLY: Only when failure would be unclear
assert param_hint is not None, f"Expected param hint to resolve, got {param_hint}"

Collection and Type Checking

# GOOD: Specific assertions
assert isinstance(result, ExpectedType)
assert hasattr(obj, "expected_attribute")
assert "key" in dictionary

# GOOD: Content validation
assert result == expected_value
assert set(actual_list) == set(expected_list)  # Order-independent

๐Ÿ“ Documentation Standards

Required Docstring Format

Every test function MUST have a docstring following this pattern:

Single-line (Simple Tests)

def test_cache_stores_values():
    """Test that HierarchicalCache stores and retrieves values correctly."""

Multi-line (Complex Tests)

def test_forward_ref_resolution_with_augmentation():
    """Test that TypeHintProvider resolves forward references with namespace augmentation.

    Validates the provider's ability to augment namespaces when forward
    references point to classes in sibling modules. Should successfully
    resolve 'MyClass' when imported from pkg.models.
    """

Regression Tests

def test_circular_forward_references_bounded_resolution():
    """Regression test for circular forward reference infinite recursion.

    Previously, circular forward references between modules caused stack overflow.
    This test ensures the bounded augmentation approach correctly resolves types
    without infinite recursion.

    Reference: commit abc123def - "fix infinite recursion in forward ref resolution"
    """

Documentation Requirements

ALL TESTS MUST HAVE DOCSTRINGS - no exceptions for new or modified tests.

  1. Purpose Statement: Start with "Test that..." or "Regression test for..."
  2. Component Identification: Clearly state what component is being tested
  3. Scenario Description: Specify the condition or scenario being validated
  4. Success Criteria: Describe what successful validation looks like
  5. References: Regression tests must include commit/PR references since we lack issue tracking

๐Ÿงช Testing Patterns and Examples

Unit Test Patterns (Function-Based with Fixtures)

Testing Components with Real Dependencies

@pytest.fixture
def cache_manager():
    """Provide TypeHintCacheManager with real dependencies."""
    cache = ConcurrentHierarchicalCache()
    cache_key_builder = CacheKeyBuilder()  # Real implementation
    return TypeHintCacheManager(cache, cache_key_builder)

def test_cache_manager_stores_and_retrieves_results(cache_manager):
    """Test that cache manager stores and retrieves results correctly."""
    # Arrange
    test_result = {"hint": str}

    # Act
    cache_manager.cache_result("callable", str, "test.func", test_result)
    retrieved = cache_manager.get_cached_result("callable", str, "test.func", dict)

    # Assert
    assert retrieved == test_result

def test_cache_manager_returns_none_for_missing_keys(cache_manager):
    """Test that cache manager returns None for non-existent cache keys."""
    # Act
    result = cache_manager.get_cached_result("callable", str, "missing.func", dict)

    # Assert
    assert result is None

Testing with Minimal Mocking (Integration Points Only)

@pytest.fixture
def metrics_emitter_with_mock():
    """Provide metrics emitter with mocked metrics for testing integration point."""
    mock_metrics = Mock(spec=ReflectionMetrics)
    return TypeHintMetricsEmitter(mock_metrics), mock_metrics

def test_metrics_emitter_calls_underlying_metrics_service(metrics_emitter_with_mock):
    """Test that metrics emitter correctly delegates to underlying metrics service."""
    emitter, mock_metrics = metrics_emitter_with_mock

    # Act
    emitter.emit_resolution_success("test.subject", "ok")

    # Assert - verify the integration point behavior
    mock_metrics.emit.assert_called_once_with(
        "type_hints.resolved", {"subject": "test.subject", "status": "ok"}
    )

@pytest.fixture
def type_hint_provider():
    """Provide HighFidelityTypeHintProvider with real dependencies."""
    cache = ConcurrentHierarchicalCache()
    test_metrics = MetricsCollector()  # Real metrics for behavior verification
    mb = MemberBuilder(cache)
    ir = PythonImportResolver(mb, test_metrics)
    return HighFidelityTypeHintProvider(ir, test_metrics, cache, strict=False)

def test_direct_resolution_succeeds_for_simple_annotations(type_hint_provider):
    """Test that direct resolution succeeds when type annotations are immediately resolvable."""
    # Arrange
    def simple_func(x: int, y: str) -> bool:
        return True

    # Act
    hints = type_hint_provider._resolve_hints_obj(
        simple_func, subject_module_name=simple_func.__module__
    )

    # Assert
    assert hints is not None
    assert hints["x"] is int
    assert hints["y"] is str
    assert hints["return"] is bool

Integration Test Patterns (No Mocks, Real Components)

Rule of Thumb for Integration Tests

  • Complexity: Test should focus on component interactions, not exhaustive scenario coverage
  • Performance: Individual integration test should complete in <500ms, entire suite in seconds
  • Scope: Test internal workflows and component collaboration, not just public APIs

End-to-End Workflow Testing

@pytest.fixture
def reflection_context():
    """Provide ReflectionContext with real components for integration testing."""
    return ReflectionContext()

def test_complete_scan_resolve_invoke_workflow(reflection_context):
    """Test complete workflow from scanning to method invocation.

    Validates that scanning a module, resolving a type, and invoking its
    methods works correctly through component collaboration.
    """
    # Arrange - use dedicated test resources
    ctx = reflection_context

    # Act - exercise multiple components working together
    module_info = ctx.scan_module("tests.resources.sample_classes")
    type_info = ctx.resolve_type("tests.resources.sample_classes.SampleClass")
    instance = ctx.create_instance(type_info, arg1="test", arg2=42)

    # Assert - verify successful collaboration
    assert module_info is not None
    assert type_info is not None
    assert instance is not None
    assert hasattr(instance, "method_to_test")

@pytest.fixture
def integration_components():
    """Provide integrated components for testing interactions."""
    cache = ConcurrentHierarchicalCache()
    test_metrics = MetricsCollector()
    mb = MemberBuilder(cache)
    ir = PythonImportResolver(mb, test_metrics)
    provider = HighFidelityTypeHintProvider(ir, test_metrics, cache, strict=False)
    return provider, test_metrics, mb

def test_cache_integration_across_multiple_resolutions(integration_components):
    """Test that cache integration works correctly across multiple type hint resolutions."""
    provider, test_metrics, mb = integration_components

    # Arrange
    def test_func(x: int, y: str) -> bool:
        return True

    import sys
    mod_info = mb.build_module(sys.modules[test_func.__module__])
    callable_info = mb.build_function(test_func, mod_info)

    # Act - first resolution (cache miss)
    test_metrics.clear()
    hints1 = provider.for_callable(callable_info)

    # Act - second resolution (cache hit)
    test_metrics.clear()
    hints2 = provider.for_callable(callable_info)

    # Assert - verify caching behavior
    assert hints1 == hints2
    assert hints1 is hints2  # Same object from cache
    # Verify fewer metrics events on cache hit (specific count depends on implementation)

Metrics-Based Testing (Never Use Logs)

Testing Behavior Frequency (Not Timing)

@pytest.fixture
def provider_with_metrics():
    """Provide type hint provider with metrics collector for behavior verification."""
    cache = ConcurrentHierarchicalCache()
    test_metrics = MetricsCollector()
    mb = MemberBuilder(cache)
    ir = PythonImportResolver(mb, test_metrics)
    provider = HighFidelityTypeHintProvider(ir, test_metrics, cache, strict=False)
    return provider, test_metrics

def test_simple_resolution_attempts_exactly_once(provider_with_metrics):
    """Test that simple type resolution is attempted exactly once, not multiple times."""
    provider, test_metrics = provider_with_metrics

    def simple_func(x: int) -> str:
        return str(x)

    # Act
    test_metrics.clear()
    provider._resolve_hints_obj(simple_func, subject_module_name=simple_func.__module__)

    # Assert - verify operation count, not timing
    test_metrics.assert_operation_count("type_hints.resolved", 1)  # Exactly once
    success_events = test_metrics.get_events_for_operation("type_hints.resolved")
    assert len(success_events) == 1
    assert success_events[0].payload["status"] == "ok"

def test_fallback_resolution_attempts_multiple_strategies(provider_with_metrics):
    """Test that fallback resolution tries multiple strategies as expected."""
    provider, test_metrics = provider_with_metrics

    def complex_func(param: "NonExistentType") -> str:  # type: ignore
        return "test"

    # Act
    test_metrics.clear()
    provider._resolve_hints_obj(complex_func, subject_module_name=complex_func.__module__)

    # Assert - verify attempt progression
    first_fail_events = test_metrics.get_events_for_operation("type_hints.first_fail")
    resolved_events = test_metrics.get_events_for_operation("type_hints.resolved")

    assert len(first_fail_events) == 1  # Initial failure
    assert len(resolved_events) == 1    # Final resolution (partial)
    assert resolved_events[0].payload["status"] == "partial"

def test_strict_mode_fails_fast_without_retry(provider_with_metrics):
    """Test that strict mode fails immediately without attempting fallback strategies."""
    provider, test_metrics = provider_with_metrics
    provider._strict = True  # Enable strict mode

    def failing_func(param: "NonExistentType") -> int:  # type: ignore
        return 1

    # Act & Assert
    test_metrics.clear()
    with pytest.raises(Exception):  # TypeHintResolutionError
        provider._resolve_hints_obj(failing_func, subject_module_name=failing_func.__module__)

    # Verify no retry attempts, immediate failure
    strict_error_events = test_metrics.get_events_for_operation("type_hints.strict_error")
    unresolved_events = test_metrics.get_events_for_operation("type_hints.unresolved")

    assert len(strict_error_events) == 1  # Immediate failure
    assert len(unresolved_events) == 0    # No fallback attempted

๐Ÿ—๏ธ Test Architecture Principles

File Organization and Limits

Industry Standard: Test File Size Limits

  • Maximum 20 test functions per file (industry standard for maintainability)
  • Split large files by logical grouping (e.g., separate files for different classes/behaviors)
  • Use descriptive file names that indicate the component/behavior being tested

Example File Organization

tests/unit/reflection/
โ”œโ”€โ”€ test_cache_manager_operations.py          # 15 tests for cache operations
โ”œโ”€โ”€ test_cache_manager_integration_points.py  # 8 tests for cache integration seams
โ”œโ”€โ”€ test_type_hint_provider_direct_resolution.py     # 12 tests for direct resolution
โ”œโ”€โ”€ test_type_hint_provider_fallback_resolution.py   # 18 tests for fallback behavior
โ””โ”€โ”€ test_metrics_emitter_events.py           # 14 tests for metrics emission

Dependency Management Principles

Unit Tests: Minimal Mocking

@pytest.fixture
def cache_with_real_dependencies():
    """Setup unit test with real dependencies, minimal mocking."""
    cache = ConcurrentHierarchicalCache()  # Real implementation
    cache_key_builder = CacheKeyBuilder()  # Real implementation
    return TypeHintCacheManager(cache, cache_key_builder)

@pytest.fixture
def metrics_emitter_testing_integration_point():
    """Setup for testing integration point with external metrics service."""
    mock_metrics = Mock(spec=ReflectionMetrics)  # Only mock the external boundary
    return TypeHintMetricsEmitter(mock_metrics), mock_metrics

Integration Tests: Real Components Only

@pytest.fixture
def full_reflection_stack():
    """Setup integration test with complete real component stack."""
    cache = ConcurrentHierarchicalCache()
    test_metrics = MetricsCollector()  # Real metrics for behavior verification
    ctx = ReflectionContext(
        cache=cache,
        metrics=test_metrics,
        type_hint_config=TypeHintConfig(strict=True),
    )
    return ctx, test_metrics

Test Data and Resources

Use Dedicated Test Packages

# GOOD: Dedicated test resources
PKG_SAMPLE = "tests.resources.sample_classes_pkg"
CLASS_SAMPLE = f"{PKG_SAMPLE}.a.SampleClass"

def test_reflection_with_sample_class():
    """Test reflection operations with dedicated test class."""
    ctx = ReflectionContext()
    type_info = ctx.resolve_type(CLASS_SAMPLE)
    assert type_info.name == "SampleClass"

Create Focused Test Modules

# tests/resources/forward_refs_pkg/models.py
class User:
    """Test class for forward reference scenarios."""
    friends: list["User"]  # Forward reference to self
    manager: "Manager"     # Forward reference to sibling class

class Manager(User):
    """Test class extending User with additional forward refs."""
    reports: list["User"]

๐Ÿ”„ Test Lifecycle and Maintenance

Setup and Teardown Patterns

Class-Level Setup (Shared Fixtures)

class TestTypeHintProvider:
    def setup_method(self):
        """Setup fresh fixtures for each test method."""
        self.cache = ConcurrentHierarchicalCache()
        self.test_metrics = MetricsCollector()
        # Fresh instances prevent test pollution

    def test_functionality(self):
        """Each test gets fresh fixtures."""
        # Use self.cache, self.test_metrics
        pass

Test-Level Cleanup

def test_with_cleanup(self):
    """Test that cleans up after itself."""
    self.test_metrics.clear()  # Clear before test

    # Test logic here

    # Implicit cleanup via fresh fixtures in setup_method

Test Naming Conventions

Function Naming Pattern: test_component_behavior_when_condition()

# EXCELLENT: Component + Behavior + Condition pattern
def test_cache_manager_returns_none_when_key_missing():
def test_scanner_discovers_classes_when_module_valid():
def test_forward_ref_resolution_succeeds_when_circular_imports():
def test_type_hint_provider_attempts_fallback_when_direct_resolution_fails():

# GOOD: Clear behavior description
def test_cache_stores_and_retrieves_values_correctly():
def test_metrics_emitter_calls_underlying_service():

# AVOID: Vague or implementation-focused names
def test_cache():                    # What about cache?
def test_scanner_works():           # Works how?
def test_forward_refs():            # What aspect of forward refs?
def test_provider_with_fallback():  # What behavior exactly?

Focus Requirement: One Behavior Per Test

# GOOD: Focused on specific behavior
def test_cache_manager_stores_results_correctly():
    """Test that cache manager stores results and retrieves them accurately."""

def test_cache_manager_returns_none_for_missing_keys():
    """Test that cache manager returns None when requested key does not exist."""

# AVOID: Testing multiple behaviors
def test_cache_manager_operations():  # Too broad - what operations?
    """Test cache operations."""      # Which operations? What's expected?
    # This test probably does too much

Edge Case Naming: Any Descriptive Name

# ACCEPTABLE: Descriptive edge case names
def test_forward_ref_resolution_with_deeply_nested_modules():
def test_type_hints_with_malformed_annotation_strings():
def test_circular_dependency_detection_boundary_conditions():
def test_unicode_class_names_in_forward_references():

โšก Performance and Reliability

Test Performance Guidelines

  • Unit tests: Target <50ms execution time per test
  • Integration tests: Target <500ms execution time per test
  • Entire test suite: Should complete in seconds, not minutes
  • Use focused test data: Small, targeted examples rather than large datasets
  • Real implementations preferred: Avoid mocking unless testing integration points

Test Reliability Principles

  • Deterministic outcomes: Tests should pass/fail consistently across environments
  • Isolated state: Tests should not depend on execution order (use fresh fixtures)
  • Clear failure messages: Specific assertions with minimal custom messages
  • Focused behavior: One specific behavior per test function

Example: Reliable Test Structure (Function-Based)

@pytest.fixture
def type_hint_provider():
    """Provide HighFidelityTypeHintProvider with real dependencies."""
    cache = ConcurrentHierarchicalCache()
    test_metrics = MetricsCollector()
    mb = MemberBuilder(cache)
    ir = PythonImportResolver(mb, test_metrics)
    return HighFidelityTypeHintProvider(ir, test_metrics, cache, strict=False)

def test_union_type_forward_references_resolve_correctly(type_hint_provider):
    """Test that Union types with forward references resolve to actual types.

    Validates resolution of Union[ForwardRef, builtin] where ForwardRef
    points to a class in the same module as the annotation.
    """
    # Arrange - create test module with Union forward reference
    test_module = types.ModuleType("test_union_module")
    union_code = '''
from typing import Union

def func_with_union(param: Union["CustomClass", int]) -> str:
    return "test"

class CustomClass:
    pass
'''
    exec(union_code, test_module.__dict__)
    test_func = test_module.__dict__["func_with_union"]

    # Act - single focused operation
    hints = type_hint_provider._resolve_hints_obj(
        test_func, subject_module_name=test_module.__name__
    )

    # Assert - specific verification with clear expectations
    assert hints is not None
    assert "param" in hints
    assert "return" in hints
    assert hints["return"] is str

    # Verify Union resolution without custom error messages (clear enough)
    param_hint = hints["param"]
    assert not isinstance(param_hint, UnresolvedForwardRef)

๐Ÿ“‹ Quick Reference Checklist

Before Writing Any Test

  • Purpose Clear: Can you explain the specific behavior being tested in one sentence?
  • Category Identified: Unit (isolated component) vs Integration (component interaction)?
  • Fixture Planned: What real dependencies are needed? Any mocks for integration points?
  • Metrics Strategy: Will you verify operation counts/behavior frequency?

Test Function Checklist

  • Naming: Follows test_component_behavior_when_condition() pattern
  • Docstring: Required for all new/modified tests - explains purpose, component, scenario, success
  • Single Behavior: Tests one specific behavior, not multiple concerns
  • Specific Assertions: Uses exact comparisons, verifies expected counts
  • Fresh Fixtures: Uses pytest fixtures, avoids test class patterns

File Organization Checklist

  • Size Limit: File contains โ‰ค20 test functions (split if larger)
  • Logical Grouping: Tests in file are related by component/behavior
  • Descriptive Naming: File name clearly indicates what component/behavior is tested
  • No Test Classes: Uses function-based tests with fixtures only

Integration vs Unit Decision Guide

Choose Unit Test When: - Testing specific class/function behavior in isolation - Verifying individual component logic - Fast execution required (<50ms) - Dependencies can be easily isolated

Choose Integration Test When: - Testing how components work together - Verifying end-to-end workflows - Testing internal component interactions - Real component behavior is essential (no mocks)