# TDD with 90%+ Coverage Patterns

## Test Structure
```python
# tests/unit/test_processor.py
import pytest
from unittest.mock import Mock, patch
from mylib.processor import DataProcessor
from mylib.exceptions import ValidationError

class TestDataProcessor:
    """Test DataProcessor class."""

    @pytest.fixture
    def processor(self):
        """Create processor instance for tests."""
        return DataProcessor(timeout=10)

    @pytest.fixture
    def sample_data(self):
        """Sample data for tests."""
        return [
            {"id": 1, "value": 100},
            {"id": 2, "value": 200},
        ]

    def test_process_valid_data(self, processor, sample_data):
        """Test processing valid data."""
        result = processor.process(sample_data)

        assert len(result) == 2
        assert result[0]["processed"] is True
        assert result[0]["value"] == 100

    def test_process_empty_data(self, processor):
        """Test processing empty data."""
        result = processor.process([])
        assert result == []

    def test_process_invalid_data(self, processor):
        """Test processing invalid data raises error."""
        invalid_data = [{"id": 1}]  # Missing 'value'

        with pytest.raises(ValidationError) as exc_info:
            processor.process(invalid_data)

        assert "Missing required field: value" in str(exc_info.value)
```

## Common Testing Patterns

### 1. Parametrized Tests
```python
@pytest.mark.parametrize("input_value,expected", [
    ("hello", "HELLO"),
    ("Hello World", "HELLO WORLD"),
    ("123", "123"),
    ("", ""),
    (None, None),
])
def test_uppercase_conversion(input_value, expected):
    """Test uppercase conversion with various inputs."""
    assert to_uppercase(input_value) == expected

@pytest.mark.parametrize("invalid_input", [
    123,
    [],
    {"key": "value"},
])
def test_uppercase_invalid_types(invalid_input):
    """Test uppercase with invalid types."""
    with pytest.raises(TypeError):
        to_uppercase(invalid_input)
```

### 2. Mock External Dependencies
```python
class TestAPIClient:
    @patch('requests.get')
    def test_fetch_data_success(self, mock_get):
        """Test successful API call."""
        # Setup mock
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"status": "ok"}
        mock_get.return_value = mock_response

        # Test
        client = APIClient()
        result = client.fetch_data("/api/test")

        # Verify
        assert result == {"status": "ok"}
        mock_get.assert_called_once_with(
            "https://api.example.com/api/test",
            timeout=30
        )

    @patch('requests.get')
    def test_fetch_data_timeout(self, mock_get):
        """Test API timeout handling."""
        mock_get.side_effect = requests.Timeout("Connection timeout")

        client = APIClient()
        with pytest.raises(NetworkTimeoutError):
            client.fetch_data("/api/test")
```

### 3. Testing Async Code
```python
import pytest
import asyncio

@pytest.mark.asyncio
async def test_async_processor():
    """Test async data processing."""
    processor = AsyncProcessor()
    data = [1, 2, 3, 4, 5]

    results = []
    async for result in processor.process_stream(data):
        results.append(result)

    assert len(results) == 5
    assert all(r["status"] == "processed" for r in results)

@pytest.mark.asyncio
async def test_concurrent_processing():
    """Test concurrent processing of multiple items."""
    processor = AsyncProcessor()

    # Process multiple items concurrently
    tasks = [
        processor.process_item(i)
        for i in range(10)
    ]
    results = await asyncio.gather(*tasks)

    assert len(results) == 10
```

### 4. Testing Error Paths
```python
def test_complete_error_coverage():
    """Test all error paths for coverage."""
    processor = DataProcessor()

    # Test validation errors
    with pytest.raises(ValidationError):
        processor.process(None)

    with pytest.raises(ValidationError):
        processor.process([{"invalid": "data"}])

    # Test processing errors
    with patch.object(processor, '_validate') as mock_validate:
        mock_validate.side_effect = Exception("Unexpected error")

        with pytest.raises(ProcessingError) as exc_info:
            processor.process([{"id": 1}])

        assert "Processing failed" in str(exc_info.value)
        assert exc_info.value.__cause__.args[0] == "Unexpected error"
```

### 5. Testing Context Managers
```python
def test_resource_manager():
    """Test resource context manager."""
    resource = Mock()

    with ResourceManager(resource) as mgr:
        mgr.use_resource()
        assert resource.open.called

    # Verify cleanup
    assert resource.close.called

def test_resource_manager_with_error():
    """Test resource cleanup on error."""
    resource = Mock()

    with pytest.raises(RuntimeError):
        with ResourceManager(resource) as mgr:
            resource.open.assert_called_once()
            raise RuntimeError("Test error")

    # Verify cleanup happened despite error
    resource.close.assert_called_once()
```

## Coverage Patterns

### 1. Branch Coverage
```python
def complex_logic(x: int, y: int) -> str:
    """Function with multiple branches."""
    if x > 0:
        if y > 0:
            return "both positive"
        else:
            return "x positive, y not"
    elif x < 0:
        return "x negative"
    else:
        return "x is zero"

# Tests for full branch coverage
def test_complex_logic_all_branches():
    assert complex_logic(1, 1) == "both positive"
    assert complex_logic(1, -1) == "x positive, y not"
    assert complex_logic(1, 0) == "x positive, y not"
    assert complex_logic(-1, 1) == "x negative"
    assert complex_logic(0, 1) == "x is zero"
```

### 2. Exception Coverage
```python
# Mark lines that shouldn't be reached
def process_data(data):
    try:
        return transform(data)
    except TransformError as e:
        logger.error(f"Transform failed: {e}")
        raise
    except Exception as e:
        # This should never happen in normal operation
        logger.critical(f"Unexpected error: {e}")  # pragma: no cover
        raise SystemError("Critical failure") from e  # pragma: no cover
```

### 3. Testing Private Methods
```python
class TestDataValidator:
    def test_private_validation_logic(self):
        """Test private methods for coverage."""
        validator = DataValidator()

        # Test private method directly for coverage
        assert validator._is_valid_email("test@example.com")
        assert not validator._is_valid_email("invalid")

        # But prefer testing through public interface
        assert validator.validate({"email": "test@example.com"})
```

## Test Fixtures

### 1. Complex Fixtures
```python
@pytest.fixture
def database(tmp_path):
    """Create temporary database for tests."""
    db_path = tmp_path / "test.db"
    conn = sqlite3.connect(db_path)

    # Setup schema
    conn.execute("""
        CREATE TABLE users (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            email TEXT UNIQUE
        )
    """)

    # Add test data
    conn.execute(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        ("Test User", "test@example.com")
    )
    conn.commit()

    yield conn

    # Cleanup
    conn.close()

def test_user_query(database):
    """Test with database fixture."""
    cursor = database.execute("SELECT * FROM users")
    users = cursor.fetchall()
    assert len(users) == 1
```

### 2. Fixture Factories
```python
@pytest.fixture
def make_user():
    """Factory fixture for creating users."""
    created_users = []

    def _make_user(**kwargs):
        defaults = {
            "name": "Test User",
            "email": f"user{len(created_users)}@example.com",
            "active": True
        }
        defaults.update(kwargs)
        user = User(**defaults)
        created_users.append(user)
        return user

    yield _make_user

    # Cleanup all created users
    for user in created_users:
        user.delete()

def test_user_interaction(make_user):
    """Test with factory fixture."""
    sender = make_user(name="Sender")
    receiver = make_user(name="Receiver")

    message = sender.send_message(receiver, "Hello")
    assert message.recipient == receiver
```

## Coverage Configuration
```toml
# pyproject.toml
[tool.coverage.run]
source = ["src"]
branch = true
omit = [
    "*/tests/*",
    "*/migrations/*",
]

[tool.coverage.report]
fail_under = 90
show_missing = true
skip_covered = false
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "if TYPE_CHECKING:",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
]

[tool.coverage.html]
directory = "htmlcov"
```
