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

  1. Isolation: Each test should be independent

  2. Descriptive Names: Test names should explain what they test

  3. Arrange-Act-Assert: Clear test structure

  4. Edge Cases: Test boundary conditions and error cases

  5. Performance: Include performance regression tests

  6. Documentation: Document complex test scenarios

  7. Maintenance: Regularly update tests as code evolves