TradingAgents/.claude/skills/testing-guide/coverage-strategies.md

10 KiB

Coverage Strategies Guide

Purpose: Strategies and techniques for achieving and maintaining 80%+ code coverage.

When to use: When planning test coverage, identifying gaps, or aiming for comprehensive test suites.


80% Coverage Target

Why 80%?

The 80% coverage threshold represents a pragmatic balance between comprehensive testing and development efficiency:

  • High confidence: Covers the vast majority of code paths
  • Practical: Achievable without diminishing returns
  • Maintainable: Doesn't require testing every trivial branch
  • Industry standard: Widely accepted as "good coverage"

Coverage Types

Line Coverage: Percentage of code lines executed during tests Branch Coverage: Percentage of decision branches (if/else) taken Function Coverage: Percentage of functions called during tests

Aim for 80%+ in all three categories.


Achieving 80%+ Coverage

1. Start with Critical Paths

Focus first on the most important code paths:

# Critical path: User authentication
def test_successful_login():
    """Test successful user login (critical path)."""
    user = authenticate("user@example.com", "password")
    assert user is not None
    assert user.is_authenticated is True

def test_failed_login():
    """Test failed login (critical error path)."""
    user = authenticate("user@example.com", "wrong_password")
    assert user is None

2. Cover Edge Cases

Identify and test boundary conditions and edge cases:

# Edge cases for string processing
@pytest.mark.parametrize("input,expected", [
    ("", ""),                    # Empty string
    ("a", "A"),                  # Single character
    ("hello", "HELLO"),          # Normal case
    ("ALREADY UPPER", "ALREADY UPPER"),  # Already uppercase
    ("123", "123"),              # Numbers only
    ("hello123", "HELLO123"),    # Mixed alphanumeric
    ("hello world", "HELLO WORLD"),  # Multiple words
    ("  spaces  ", "  SPACES  "),  # Leading/trailing spaces
])
def test_uppercase_edge_cases(input, expected):
    """Test uppercase conversion with edge cases."""
    assert to_uppercase(input) == expected

3. Test Error Handling

Error paths are often missed in coverage. Test all exception scenarios:

def test_division_by_zero():
    """Test error handling for division by zero."""
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_invalid_file_path():
    """Test error handling for invalid file path."""
    with pytest.raises(FileNotFoundError):
        read_file("/nonexistent/path.txt")

def test_network_timeout():
    """Test error handling for network timeout."""
    with pytest.raises(requests.Timeout):
        fetch_data(timeout=0.001)

def test_invalid_input_validation():
    """Test validation error for invalid input."""
    with pytest.raises(ValueError, match="Input must be positive"):
        validate_input(-1)

4. Test Boundary Conditions

Test values at the edges of valid ranges:

@pytest.mark.parametrize("age,valid", [
    (0, True),     # Minimum valid
    (1, True),     # Just above minimum
    (17, False),   # Just below threshold
    (18, True),    # Threshold
    (19, True),    # Just above threshold
    (120, True),   # Maximum reasonable
    (121, False),  # Above maximum
    (-1, False),   # Below minimum
])
def test_age_validation_boundaries(age, valid):
    """Test age validation at boundary conditions."""
    assert is_valid_age(age) == valid

5. Test All Branches

Ensure every if/else branch is tested:

def process_status(status):
    """Process status with multiple branches."""
    if status == "active":
        return "Processing active status"
    elif status == "pending":
        return "Processing pending status"
    elif status == "completed":
        return "Processing completed status"
    else:
        return "Unknown status"

# Test all branches
def test_process_status_active():
    assert process_status("active") == "Processing active status"

def test_process_status_pending():
    assert process_status("pending") == "Processing pending status"

def test_process_status_completed():
    assert process_status("completed") == "Processing completed status"

def test_process_status_unknown():
    assert process_status("unknown") == "Unknown status"

Coverage Tools and Configuration

pytest-cov

# Install pytest-cov
pip install pytest-cov

# Run tests with coverage report
pytest --cov=mypackage tests/

# Generate HTML coverage report
pytest --cov=mypackage --cov-report=html tests/

# Fail if coverage below 80%
pytest --cov=mypackage --cov-fail-under=80 tests/

coverage.py Configuration

Create .coveragerc file:

[run]
source = mypackage
omit =
    */tests/*
    */migrations/*
    */__pycache__/*
    */venv/*

[report]
precision = 2
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError
    if __name__ == .__main__.:
    if TYPE_CHECKING:

Measuring Coverage

# Example: Measure coverage for specific module
pytest --cov=mypackage.auth --cov-report=term-missing tests/test_auth.py

# Output shows uncovered lines:
# mypackage/auth.py    85%   23, 45, 67

Strategies for Hard-to-Test Code

Strategy 1: Extract Logic

Move complex logic into testable functions:

# Before: Hard to test
def process_request(request):
    if request.user.is_authenticated and request.method == "POST":
        data = json.loads(request.body)
        if validate_data(data):
            save_to_database(data)
            return HttpResponse("Success")
    return HttpResponse("Error")

# After: Testable components
def is_valid_request(user, method):
    """Check if request is valid (easily testable)."""
    return user.is_authenticated and method == "POST"

def test_is_valid_request():
    """Test request validation logic."""
    user = Mock(is_authenticated=True)
    assert is_valid_request(user, "POST") is True
    assert is_valid_request(user, "GET") is False

Strategy 2: Dependency Injection

Make dependencies explicit for easier mocking:

# Before: Hard to test (hardcoded dependency)
def fetch_user_data(user_id):
    db = Database()  # Hard to mock
    return db.query(user_id)

# After: Testable with dependency injection
def fetch_user_data(user_id, database=None):
    db = database or Database()
    return db.query(user_id)

def test_fetch_user_data():
    """Test with injected mock database."""
    mock_db = Mock()
    mock_db.query.return_value = {"id": 1, "name": "Test"}
    result = fetch_user_data(1, database=mock_db)
    assert result["name"] == "Test"

Strategy 3: Mock External Dependencies

Replace external systems with mocks:

@patch('mypackage.api.requests.get')
def test_external_api_call(mock_get):
    """Test function that calls external API."""
    # Arrange
    mock_response = Mock()
    mock_response.json.return_value = {"status": "ok"}
    mock_get.return_value = mock_response

    # Act
    result = fetch_external_data("https://api.example.com")

    # Assert
    assert result["status"] == "ok"
    mock_get.assert_called_once_with("https://api.example.com")

Identifying Coverage Gaps

1. Use Coverage Reports

# Generate detailed HTML report
pytest --cov=mypackage --cov-report=html tests/

# Open htmlcov/index.html to see:
# - Red lines: Not covered
# - Green lines: Covered
# - Yellow lines: Partially covered (branch coverage)

2. Focus on Red Lines

Prioritize testing the most critical uncovered lines first.

3. Check Branch Coverage

# Show branch coverage details
pytest --cov=mypackage --cov-report=term-missing --cov-branch tests/

Maintaining High Coverage

1. Make Coverage Part of CI/CD

# .github/workflows/test.yml
- name: Run tests with coverage
  run: pytest --cov=mypackage --cov-fail-under=80 tests/

2. Review Coverage in Pull Requests

Use tools like Codecov or Coveralls to track coverage changes in PRs.

3. Write Tests First (TDD)

Test-Driven Development naturally leads to high coverage:

  1. Write failing test
  2. Write minimal code to pass
  3. Refactor
  4. Result: Every line has a test

4. Avoid "Coverage Gaming"

Don't write useless tests just to increase coverage percentage. Focus on meaningful tests that verify behavior.


Practical Example: Achieving 80% Coverage

# Function to test
def calculate_discount(price, customer_type, quantity):
    """Calculate discount based on customer type and quantity."""
    if price <= 0:
        raise ValueError("Price must be positive")

    if customer_type == "premium":
        discount = 0.20
    elif customer_type == "regular":
        discount = 0.10
    else:
        discount = 0.0

    if quantity >= 10:
        discount += 0.05

    final_price = price * (1 - discount)
    return round(final_price, 2)

# Comprehensive test suite (80%+ coverage)
class TestCalculateDiscount:
    """Test discount calculation with full coverage."""

    def test_premium_customer_small_quantity(self):
        """Test premium customer with quantity < 10."""
        assert calculate_discount(100, "premium", 5) == 80.0

    def test_premium_customer_bulk_quantity(self):
        """Test premium customer with quantity >= 10."""
        assert calculate_discount(100, "premium", 10) == 75.0

    def test_regular_customer_small_quantity(self):
        """Test regular customer with quantity < 10."""
        assert calculate_discount(100, "regular", 5) == 90.0

    def test_regular_customer_bulk_quantity(self):
        """Test regular customer with quantity >= 10."""
        assert calculate_discount(100, "regular", 10) == 85.0

    def test_guest_customer_small_quantity(self):
        """Test guest customer with quantity < 10."""
        assert calculate_discount(100, "guest", 5) == 100.0

    def test_guest_customer_bulk_quantity(self):
        """Test guest customer with quantity >= 10."""
        assert calculate_discount(100, "guest", 10) == 95.0

    def test_invalid_price(self):
        """Test error handling for invalid price."""
        with pytest.raises(ValueError, match="Price must be positive"):
            calculate_discount(0, "premium", 5)

    def test_negative_price(self):
        """Test error handling for negative price."""
        with pytest.raises(ValueError, match="Price must be positive"):
            calculate_discount(-10, "premium", 5)

# Result: 100% line coverage, 100% branch coverage

For more details: See SKILL.md for complete testing methodology and pytest-patterns.md for testing techniques.