Skip to content

Latest commit

 

History

History
173 lines (126 loc) · 3.53 KB

File metadata and controls

173 lines (126 loc) · 3.53 KB

Testing & Test-Driven Development

TDD Philosophy

Test-Driven Development follows the Red-Green-Refactor cycle:

  1. Red: Write failing test
  2. Green: Write minimal code to pass test
  3. Refactor: Improve code while keeping tests passing

Test Structure

Unit Tests

Test individual functions in isolation:

@pytest.mark.unit
def test_validate_record_success() -> None:
    """Test validating valid record."""
    data = {"id": 1, "value": 10.5, "label": "test"}
    record = DataValidator.validate_record(data)
    assert record.id == 1

Integration Tests

Test multiple components working together:

@pytest.mark.integration
def test_end_to_end_pipeline(temp_dir: Path) -> None:
    """Test complete pipeline flow."""
    # Setup
    input_file = temp_dir / "input.csv"
    
    # Execute
    pipeline = DataPipeline()
    result = pipeline.execute(input_file)
    
    # Assert
    assert result.success is True

Writing Tests

AAA Pattern

def test_feature() -> None:
    # Arrange: Setup test data
    df = pd.DataFrame({"value": [1, 2, 3]})
    
    # Act: Execute function
    result = process(df)
    
    # Assert: Verify results
    assert len(result) == 3

Using Fixtures

@pytest.fixture
def sample_data() -> pd.DataFrame:
    """Create sample data."""
    return pd.DataFrame({"id": [1, 2, 3]})

def test_with_fixture(sample_data: pd.DataFrame) -> None:
    """Test using fixture."""
    assert len(sample_data) == 3

Parametrized Tests

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
])
def test_multiply(input: int, expected: int) -> None:
    """Test multiplication."""
    assert input * 2 == expected

Running Tests

# All tests
pytest

# With coverage report
pytest --cov=src/ai_project --cov-report=html

# Specific test file
pytest tests/unit/test_config.py

# Specific test
pytest tests/unit/test_config.py::TestSettings::test_default_settings

# Unit tests only
pytest -m unit

# Integration tests only
pytest -m integration

# Verbose output
pytest -v

# Stop on first failure
pytest -x

# Show print statements
pytest -s

Coverage Requirements

  • Minimum 80% overall coverage
  • All public functions must be tested
  • Critical paths must have 100% coverage
# Generate coverage report
pytest --cov=src/ai_project --cov-report=html

# View report
open htmlcov/index.html

Mocking

Use pytest-mock for mocking:

def test_with_mock(mocker):
    """Test with mocked dependency."""
    mock_loader = mocker.patch("ai_project.processors.loader.DataLoader.load_csv")
    mock_loader.return_value = pd.DataFrame({"id": [1, 2, 3]})
    
    result = mock_loader("dummy.csv")
    assert len(result) == 3

Async Testing

@pytest.mark.asyncio
async def test_async_function() -> None:
    """Test async function."""
    result = await async_process()
    assert result is not None

Best Practices

  1. Test one thing per test - Single responsibility
  2. Use descriptive names - test_validate_record_with_negative_value_fails
  3. Keep tests fast - Mock external dependencies
  4. Test edge cases - Empty data, nulls, invalid input
  5. Avoid test interdependence - Each test should be independent
  6. Use fixtures for setup - DRY principle
  7. Test error paths - Verify exceptions are raised correctly

Continuous Integration

Tests run automatically on:

  • Pull requests
  • Commits to main branch
  • Scheduled daily runs

See .github/workflows/ for CI configuration.