Testing Guide
Comprehensive testing strategies and practices for the heterodyne package.
Test Organization
The test suite is organized hierarchically:
heterodyne/tests/
├── unit/ # Unit tests
│ ├── test_config.py # Configuration tests
│ ├── test_models.py # Model function tests
│ ├── test_optimization.py # Optimization tests
│ └── test_utils.py # Utility function tests
├── integration/ # Integration tests
│ ├── test_full_workflow.py # End-to-end workflow
│ └── test_performance.py # Performance benchmarks
├── fixtures/ # Test data and fixtures
│ ├── sample_configs/ # Sample configurations
│ ├── synthetic_data/ # Generated test data
│ └── reference_results/ # Expected results
└── conftest.py # Shared fixtures
Running Tests
All Tests:
# Run complete test suite
pytest heterodyne/tests/ -v
# With coverage
pytest heterodyne/tests/ --cov=heterodyne --cov-report=html
Specific Test Categories:
# Unit tests only
pytest heterodyne/tests/unit/ -v
# Integration tests only
pytest heterodyne/tests/integration/ -v
# Quick tests only
pytest heterodyne/tests/ -m "not slow"
Parallel Testing:
# Install pytest-xdist
pip install pytest-xdist
# Run tests in parallel
pytest heterodyne/tests/ -n 4
Test Fixtures
Common Fixtures (in conftest.py):
import pytest
import numpy as np
from heterodyne import ConfigManager
@pytest.fixture
def basic_config():
"""Basic configuration for testing"""
return {
"analysis_settings": {
"static_mode": False
},
"initial_parameters": {
"values": [100.0, -0.5, 10.0, 0.1, 0.0, 0.01, 0.5, 0.0, 50.0, 0.3, 0.0, 0.0, 0.0, 0.0]
}
}
@pytest.fixture
def synthetic_heterodyne_data():
"""Synthetic data for heterodyne model"""
tau = np.logspace(-6, 1, 100)
# 14-parameter heterodyne model
params = [100.0, -0.5, 10.0, 0.1, 0.0, 0.01, 0.5, 0.0, 50.0, 0.3, 0.0, 0.0, 0.0, 0.0]
q = 0.001
# Generate heterodyne correlation (simplified for testing)
g1 = np.exp(-q**2 * (params[0] * tau**(-params[1]) + params[2] * tau))
# Add realistic noise
noise = np.random.normal(0, 0.01, size=g1.shape)
g1_noisy = g1 + noise
return tau, g1_noisy, params, q
@pytest.fixture
def config_manager(basic_config, tmp_path):
"""ConfigManager instance for testing"""
config_file = tmp_path / "test_config.json"
with open(config_file, 'w') as f:
json.dump(basic_config, f)
return ConfigManager(str(config_file))
Unit Testing
Model Function Tests:
# test_models.py
import pytest
import numpy as np
from heterodyne.models import heterodyne_model
class TestHeterodyneModel:
def test_basic_functionality(self):
tau = np.logspace(-6, 1, 100)
# 14-parameter heterodyne model
params = [100.0, -0.5, 10.0, 0.1, 0.0, 0.01, 0.5, 0.0, 50.0, 0.3, 0.0, 0.0, 0.0, 0.0]
q = 0.001
g1 = heterodyne_model(tau, params, q)
# Basic checks
assert len(g1) == len(tau)
assert np.all(np.isfinite(g1))
def test_parameter_bounds(self):
tau = np.logspace(-6, 1, 10)
q = 0.001
# Test with valid 14-parameter set
params = [100.0, -0.5, 10.0, 0.1, 0.0, 0.01, 0.5, 0.0, 50.0, 0.3, 0.0, 0.0, 0.0, 0.0]
g1 = heterodyne_model(tau, params, q)
assert np.all(np.isfinite(g1))
Configuration Tests:
# test_config.py
from heterodyne.config import ConfigManager
from heterodyne.utils import ConfigurationError
class TestConfigManager:
def test_valid_config(self, basic_config, tmp_path):
config_file = tmp_path / "valid.json"
with open(config_file, 'w') as f:
json.dump(basic_config, f)
config = ConfigManager(str(config_file))
assert config.validate() is True
def test_invalid_config(self, tmp_path):
invalid_config = {"invalid": "structure"}
config_file = tmp_path / "invalid.json"
with open(config_file, 'w') as f:
json.dump(invalid_config, f)
with pytest.raises(ConfigurationError):
ConfigManager(str(config_file))
def test_missing_file(self):
with pytest.raises(FileNotFoundError):
ConfigManager("nonexistent.json")
Optimization Tests:
# test_optimization.py
from heterodyne.analysis.core import HeterodyneAnalysisCore
from heterodyne.optimization.classical import ClassicalOptimizer
class TestClassicalOptimization:
def test_optimization_convergence(self, config_manager,
synthetic_heterodyne_data):
phi_angles, c2_data, true_params, q = synthetic_heterodyne_data
core = HeterodyneAnalysisCore(config_manager)
# Run classical optimization
optimizer = ClassicalOptimizer(core, config_manager)
params, result = optimizer.run_classical_optimization_optimized(
phi_angles=phi_angles,
c2_experimental=c2_data
)
# Check convergence
assert result.success
assert result.chi_squared < 0.1 # Good fit
# Check parameter recovery (within 10%)
recovered_params = params
for i, (recovered, true) in enumerate(zip(recovered_params, true_params)):
relative_error = abs(recovered - true) / true
assert relative_error < 0.1, f"Parameter {i} error too large"
Integration Testing
Full Workflow Tests:
# test_full_workflow.py
import tempfile
import json
from pathlib import Path
class TestFullWorkflow:
def test_complete_heterodyne_analysis(self, synthetic_heterodyne_data):
tau, g1_data, true_params, q = synthetic_heterodyne_data
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
# Create test data files
data_file = tmp_path / "test_data.npz"
np.savez(data_file, tau=tau, g1=g1_data, q=q)
# Create configuration
config = {
"analysis_settings": {
"static_mode": True,
"static_submode": "isotropic"
},
"file_paths": {
"c2_data_file": str(data_file)
},
"initial_parameters": {
"values": [1200, -0.6, 80] # Slightly off true values
}
}
config_file = tmp_path / "config.json"
with open(config_file, 'w') as f:
json.dump(config, f)
# Run complete analysis
config_manager = ConfigManager(str(config_file))
core = HeterodyneAnalysisCore(config_manager)
core.load_experimental_data()
# Use ClassicalOptimizer for optimization
from heterodyne.optimization.classical import ClassicalOptimizer
optimizer = ClassicalOptimizer(core, config_manager.config)
params, result = optimizer.run_classical_optimization_optimized(
phi_angles=phi_angles, c2_experimental=c2_data)
# Verify results
assert result.success
assert result.chi_squared < 0.05 # Excellent fit for synthetic data
# Check parameter recovery
for recovered, true in zip(params, true_params):
assert abs(recovered - true) / true < 0.05
@pytest.mark.slow
tau, g1_data, true_params, q = synthetic_heterodyne_data
config_manager.config["optimization_config"] = {
"enabled": True,
"draws": 500, # Reduced for testing
"tune": 200,
"chains": 2
}
}
core = HeterodyneAnalysisCore(config_manager)
core._tau = tau
core._g1_data = g1_data
core._q = q
# Run classical first
from heterodyne.optimization.classical import ClassicalOptimizer
optimizer = ClassicalOptimizer(core, config_manager.config)
params, result = optimizer.run_classical_optimization_optimized(
phi_angles=phi_angles, c2_experimental=c2_data)
# Check convergence
# Check parameter uncertainties are reasonable
for param_name in posterior_means.keys():
mean_val = posterior_means[param_name]
std_val = posterior_stds[param_name]
# Uncertainty should be reasonable (not too large)
cv = std_val / abs(mean_val) # Coefficient of variation
assert cv < 0.5, f"Parameter {param_name} uncertainty too large"
Performance Testing
Benchmark Tests:
# test_performance.py
import time
import pytest
class TestPerformance:
@pytest.mark.benchmark
def test_optimization_speed(self, config_manager, synthetic_heterodyne_data):
"""Test that optimization completes within reasonable time"""
tau, g1_data, true_params, q = synthetic_heterodyne_data
core = HeterodyneAnalysisCore(config_manager)
core._tau = tau
core._g1_data = g1_data
core._q = q
start_time = time.time()
# Use ClassicalOptimizer for optimization
from heterodyne.optimization.classical import ClassicalOptimizer
optimizer = ClassicalOptimizer(core, config_manager.config)
params, result = optimizer.run_classical_optimization_optimized(
phi_angles=phi_angles, c2_experimental=c2_data)
end_time = time.time()
# Should complete within 30 seconds
assert end_time - start_time < 30
assert result.success
@pytest.mark.parametrize("dataset_size", [100, 500, 1000])
def test_scaling_performance(self, dataset_size):
"""Test performance scaling with dataset size"""
tau = np.logspace(-6, 1, dataset_size)
# ... generate data of specified size ...
# Measure performance and ensure reasonable scaling
Test Data Management
Synthetic Data Generation:
# test_data_generator.py
def generate_test_data(model_type="heterodyne", noise_level=0.01):
"""Generate synthetic test data"""
tau = np.logspace(-6, 1, 100)
if model_type == "heterodyne":
# 14-parameter heterodyne model
params = [100.0, -0.5, 10.0, 0.1, 0.0, 0.01, 0.5, 0.0, 50.0, 0.3, 0.0, 0.0, 0.0, 0.0]
g1_perfect = heterodyne_model(tau, params, 0.001)
# Add noise
noise = np.random.normal(0, noise_level, size=g1_perfect.shape)
g1_noisy = g1_perfect + noise
return tau, g1_noisy, params
Reference Data:
Store reference results for regression testing:
# Store expected results
reference_results = {
"heterodyne_basic": {
"parameters": [100.0, -0.5, 10.0, 0.1, 0.0, 0.01, 0.5, 0.0, 50.0, 0.3, 0.0, 0.0, 0.0, 0.0],
"chi_squared": 0.023,
"success": True
}
}
def test_regression(self):
# Compare current results with reference
current_result = run_analysis()
reference = reference_results["heterodyne_basic"]
for i, (current, expected) in enumerate(
zip(current_params, reference["parameters"])
):
assert abs(current - expected) / expected < 0.01
Test Configuration
pytest.ini:
[tool:pytest]
testpaths = heterodyne/tests
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
benchmark: marks performance benchmark tests
integration: marks integration tests
addopts =
--strict-markers
--strict-config
--disable-warnings
Test Dependencies:
# test-requirements.txt
pytest>=6.0
pytest-cov>=2.0
pytest-xdist>=2.0 # Parallel testing
pytest-benchmark>=3.0 # Performance testing
pytest-mock>=3.0 # Mocking utilities
hypothesis>=6.0 # Property-based testing
Continuous Integration
GitHub Actions Example:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -e .[dev]
pip install -r test-requirements.txt
- name: Run tests
run: |
pytest heterodyne/tests/ --cov=heterodyne --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
Test Best Practices
Isolation: Each test should be independent
Descriptive Names: Test names should explain what they test
Arrange-Act-Assert: Clear test structure
Edge Cases: Test boundary conditions and error cases
Performance: Include performance regression tests
Documentation: Document complex test scenarios
Maintenance: Regularly update tests as code evolves