Testing Guide
Modern testing practices for Are You Not Entertained (AYNE) using pytest with comprehensive coverage reporting.
Quick Start
# Run all tests with coverage
uv run pytest --cov=src --cov-report=html
# Run with verbose output
uv run pytest -v
# Run specific test
uv run pytest tests/unit/test_config.py::TestAnalysisConfig::test_valid_config
Test Organization
tests/
├── conftest.py # Shared fixtures and configuration
├── unit/ # Fast, isolated unit tests
│ ├── test_calendar.py
│ ├── test_config.py
│ ├── test_directory.py
│ └── test_exceptions.py
└── integration/ # End-to-end integration tests
└── test_config_integration.py
Running Tests
By Test Type
# Unit tests only (fast)
uv run pytest tests/unit/ -v
# Integration tests only
uv run pytest tests/integration/ -v
# Specific test file
uv run pytest tests/unit/test_calendar.py -v
# Specific test class
uv run pytest tests/unit/test_config.py::TestAnalysisConfig -v
# Single test method
uv run pytest tests/unit/test_calendar.py::TestCalendarFunctions::test_leap_year -v
With Markers
# Run only slow tests
uv run pytest -m slow
# Skip slow tests
uv run pytest -m "not slow"
# Run tests for specific component
uv run pytest -m config
Coverage Reports
# Generate HTML coverage report
uv run pytest --cov=src --cov-report=html
# Terminal output with missing lines
uv run pytest --cov=src --cov-report=term-missing
# Generate XML for CI/CD
uv run pytest --cov=src --cov-report=xml
# View HTML report (Windows)
start reports/coverage/html/index.html
Coverage Targets:
- Overall: 80% minimum for production
- Critical modules: 90%+ (config, io, models)
- Current baseline: 10%
Test Fixtures
Common fixtures available in conftest.py:
# Directory fixtures
temp_data_dir # Temporary directory for test data
test_settings # Complete Settings object with test defaults
# Configuration fixtures
test_analysis_config # AnalysisConfig with test values
sample_year # Year: 2025
sample_month # Month: 9
# Mock fixtures
mock_s3_client # Mocked boto3 S3 client
mock_athena_client # Mocked boto3 Athena client
Using Fixtures
def test_example(temp_data_dir, test_settings):
"""Test using fixtures."""
# temp_data_dir is a Path object to temporary directory
assert temp_data_dir.exists()
# test_settings has pre-configured paths
assert test_settings.paths.data_root == temp_data_dir
assert test_settings.analysis.year == 2025
assert test_settings.analysis.month == 9
Writing Tests
Best Practices
- One test, one behavior - Each test verifies a single thing
- Use descriptive names -
test_get_days_in_month_february_leap_year - Arrange-Act-Assert - Clear test structure
- Leverage fixtures - DRY principle for setup/teardown
- Test edge cases - Boundary conditions, errors, empty inputs
- Mock external dependencies - S3, Athena, network calls
Unit Test Example
import pytest
from ayne.utils.io import save_dataframe, load_dataframe
class TestIOFunctions:
"""Test calendar utility functions."""
def test_get_days_in_month_february_leap_year(self):
"""February in leap year has 29 days."""
assert get_days_in_month(2024, 2) == 29
def test_get_days_in_month_february_non_leap_year(self):
"""February in non-leap year has 28 days."""
assert get_days_in_month(2025, 2) == 28
def test_get_days_in_month_invalid_month(self):
"""Invalid month raises ValueError."""
with pytest.raises(ValueError, match="Month must be between 1 and 12"):
get_days_in_month(2025, 13)
Integration Test Example
from ayne.config.schema import Settings
from ayne.utils.directory import DirectoryManager
class TestConfigurationIntegration:
"""Test integration between config and directory management."""
def test_settings_with_directory_manager(self, test_settings, temp_data_dir):
"""Settings and DirectoryManager work together correctly."""
dm = DirectoryManager(test_settings.paths.data_root)
structure = dm.create_full_structure(
test_settings.analysis.year,
test_settings.analysis.month
)
# Verify directory structure created
assert structure["input"]["weights"].exists()
assert structure["output"]["durations"].exists()
assert structure["output"]["fusion"].exists()
Parametrized Tests
Test multiple inputs efficiently:
import pytest
@pytest.mark.parametrize("year,month,expected", [
(2024, 1, 31), # January
(2024, 2, 29), # Leap year February
(2025, 2, 28), # Non-leap year February
(2024, 4, 30), # April
(2024, 12, 31), # December
])
def test_days_in_month(year, month, expected):
"""Test days in month for various cases."""
assert get_days_in_month(year, month) == expected
Mocking External Services
from unittest.mock import Mock, patch
import pytest
class TestS3Download:
"""Test S3 download functionality."""
@patch('boto3.client')
def test_download_weights(self, mock_boto_client, temp_data_dir):
"""Test successful S3 download."""
# Arrange
mock_s3 = Mock()
mock_boto_client.return_value = mock_s3
# Act
downloader = S3Downloader(bucket="test-bucket")
result = downloader.download_file("key", temp_data_dir / "file.txt")
# Assert
mock_s3.download_file.assert_called_once()
assert result.exists()
Coverage Status
Current Coverage by Module
| Module | Coverage | Status | Priority |
|---|---|---|---|
utils.calendar |
100% | ✅ Complete | - |
utils.exceptions |
100% | ✅ Complete | - |
utils.types |
100% | ✅ Complete | - |
config.schema |
81% | 🟡 Good | Increase to 90% |
utils.directory |
66% | 🟡 Good | Increase to 80% |
config.loader |
58% | 🟡 Partial | Increase to 80% |
io.s3 |
0% | ❌ None | High priority |
io.athena |
0% | ❌ None | High priority |
models.fusion |
0% | ❌ None | Medium priority |
Testing Priorities
Phase 1 - Core Foundation (Current):
- Configuration loading/validation
- Directory management
- Calendar utilities
Phase 2 - I/O Operations:
- S3 download/upload (with mocks)
- Athena query execution (with mocks)
- File readers/writers
Phase 3 - Data Processing:
- Weight processing
- Hours aggregation
- Duration calculation
Phase 4 - ML & Fusion:
- LightGBM training
- Imputation logic
- Validation metrics
CI/CD Integration
Tests run automatically in GitHub Actions:
- name: Run all tests with coverage
run: |
uv run pytest tests/ -v \
--cov=src \
--cov-report=term-missing \
--cov-report=html:reports/coverage/html \
--cov-report=xml:reports/coverage/coverage.xml \
--cov-fail-under=10 \
--tb=short
- name: Upload coverage reports
uses: codecov/codecov-action@v4
with:
file: ./reports/coverage/coverage.xml
See .github/workflows/python-tests.yml for the complete test workflow.
Test Configuration
pytest.ini
Configuration in pyproject.toml:
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = [
"-v",
"--strict-markers",
"--cov-report=term-missing",
]
markers = [
"slow: marks tests as slow",
"integration: integration tests",
"unit: unit tests",
]
Troubleshooting
Common Issues
Issue: ModuleNotFoundError: No module named 'ayne'
Issue: Tests pass locally but fail in CI
- Check Python version consistency
- Verify all dependencies in
pyproject.toml - Review test isolation (files created/modified)
Issue: Coverage report not generated
# Clean and regenerate
Remove-Item -Recurse -Force .coverage, reports/coverage/
uv run pytest --cov=src --cov-report=html
Clear Test Caches
# Remove pytest cache
Remove-Item -Recurse -Force .pytest_cache
# Remove coverage data
Remove-Item -Recurse -Force .coverage, reports/coverage/
# Remove Python cache
Get-ChildItem -Recurse -Filter "__pycache__" | Remove-Item -Recurse -Force
Related Documentation
- Linting Guide - Code quality checks
- Formatting Guide - Code formatting standards
- Code Style - General style guidelines
- GitHub Actions Workflows - See
.github/workflows/python-tests.ymlfor automated testing
Next Steps
- Increase coverage - Focus on I/O and transformation modules
- Add integration tests - Test complete pipeline steps
- Performance tests - Benchmark critical operations
- Property-based testing - Use Hypothesis for edge cases