Skip to content

Navigation Menu

Sign in
Sign up

Testing Guide

github-actions[bot] edited this page Nov 15, 2025 · 2 revisions

Testing Guide

Comprehensive guide to running and writing tests for DVOACAP-Python.

Table of Contents


Test Overview

DVOACAP-Python uses pytest as its testing framework. The test suite includes:

  • Unit tests - Individual function/class testing
  • Integration tests - Multi-module interaction testing
  • Validation tests - Comparison with reference VOACAP output
  • Regression tests - Ensure bugs don't reappear

Test Structure:

dvoacap-python/
├── tests/ # pytest test directory
│ ├── test_path_geometry.py # Phase 1 tests
│ ├── test_ionospheric.py # Phase 3 tests
│ └── test_voacap_parser.py # VOACAP reference parser tests
├── test_voacap_reference.py # Full validation suite
├── test_phase5_api.py # Phase 5 API tests
├── test_mode_selection.py # Mode selection tests
├── test_mode_alignment.py # Mode alignment validation
└── test_high_freqs.py # High frequency edge cases

Running Tests

Basic Test Execution

Run all tests:

# From repository root
pytest
# Should output:
# ======================== test session starts =========================
# collected 42 items
#
# tests/test_path_geometry.py ........ [ 19%]
# tests/test_ionospheric.py .............. [ 52%]
# tests/test_voacap_parser.py .................... [100%]
#
# ======================== 42 passed in 2.34s =========================

Run with verbose output:

pytest -v
# Shows each test individually:
# tests/test_path_geometry.py::test_distance_calculation PASSED [ 2%]
# tests/test_path_geometry.py::test_azimuth_calculation PASSED [ 4%]
# ...

Running Specific Tests

Run single test file:

pytest tests/test_path_geometry.py

Run specific test function:

pytest tests/test_path_geometry.py::test_distance_calculation

Run specific test class:

pytest tests/test_ionospheric.py::TestIonosphericProfile

Run tests matching a pattern:

# Run all tests with "distance" in the name
pytest -k distance
# Run all tests with "muf" in the name
pytest -k muf

Test Output Options

Verbose output with details:

pytest -v --tb=short

Show print statements:

pytest -s
# Or combine with verbose
pytest -sv

Stop at first failure:

pytest -x

Run last failed tests only:

pytest --lf

Run failed tests first:

pytest --ff

Test Types

Unit Tests

Test individual functions and classes in isolation.

Example: Path Geometry Tests

# tests/test_path_geometry.py
import pytest
import math
from dvoacap.path_geometry import GeoPoint, PathGeometry
class TestGeoPoint:
 """Test GeoPoint class"""
 def test_from_degrees(self):
 """Test creating GeoPoint from degrees"""
 point = GeoPoint.from_degrees(40.0, -75.0)
 assert abs(point.lat - math.radians(40.0)) < 1e-9
 assert abs(point.lon - math.radians(-75.0)) < 1e-9
 def test_to_degrees(self):
 """Test converting GeoPoint to degrees"""
 point = GeoPoint.from_degrees(51.5, -0.1)
 lat, lon = point.to_degrees()
 assert abs(lat - 51.5) < 1e-9
 assert abs(lon - -0.1) < 1e-9
class TestPathGeometry:
 """Test PathGeometry class"""
 def test_distance_calculation(self):
 """Test great circle distance calculation"""
 tx = GeoPoint.from_degrees(40.0, -75.0) # Philadelphia
 rx = GeoPoint.from_degrees(51.5, -0.1) # London
 path = PathGeometry()
 path.set_tx_rx(tx, rx)
 # Expected distance ~5232 km
 distance_km = path.get_distance_km()
 assert 5200 < distance_km < 5260
 def test_azimuth_calculation(self):
 """Test azimuth calculation"""
 tx = GeoPoint.from_degrees(40.0, -75.0)
 rx = GeoPoint.from_degrees(51.5, -0.1)
 path = PathGeometry()
 path.set_tx_rx(tx, rx)
 azimuth_deg = path.get_azimuth_tr_degrees()
 # Expected azimuth ~52°
 assert 50 < azimuth_deg < 55

Run unit tests:

pytest tests/test_path_geometry.py -v

Integration Tests

Test interactions between multiple modules.

Example: Full Prediction Test

# test_phase5_api.py (simplified)
from dvoacap import PredictionEngine
from dvoacap.path_geometry import GeoPoint
import numpy as np
def test_full_prediction():
 """Test complete prediction pipeline"""
 engine = PredictionEngine()
 # Configure
 engine.params.ssn = 100.0
 engine.params.month = 6
 engine.params.tx_power = 100.0
 engine.params.tx_location = GeoPoint.from_degrees(40.0, -75.0)
 # Run prediction
 rx_location = GeoPoint.from_degrees(51.5, -0.1)
 frequencies = [7.0, 14.0, 21.0]
 engine.predict(
 rx_location=rx_location,
 utc_time=0.5,
 frequencies=frequencies
 )
 # Verify results
 assert engine.muf_calculator.muf > 0
 assert len(engine.predictions) == 3
 for pred in engine.predictions:
 assert pred.signal.snr_db is not None
 assert 0 <= pred.signal.reliability <= 1

Run integration tests:

pytest test_phase5_api.py -v

Validation Tests

Compare DVOACAP output with reference VOACAP data.

Full validation suite:

# Run complete validation
python3 test_voacap_reference.py
# Sample output:
# ======================== Validation Results =========================
# Total test cases: 500
# Passed: 419 (83.8%)
# Failed: 81 (16.2%)
#
# Phase 1 (Path Geometry): 100% pass
# Phase 2 (Solar/Geomag): 100% pass
# Phase 3 (Ionospheric): 98.5% pass
# Phase 4 (MUF/Raytracing): 95.2% pass
# Phase 5 (Signal Predictions): 83.8% pass
# =====================================================================

Run specific validation tests:

# Test mode selection
pytest test_mode_selection.py -v
# Test high frequency edge cases
pytest test_high_freqs.py -v

See Validation Status for detailed validation results.


Writing Tests

Test Structure

Follow pytest conventions:

# tests/test_my_module.py
import pytest
from dvoacap.my_module import MyClass
class TestMyClass:
 """Tests for MyClass - use docstrings"""
 def setup_method(self):
 """Run before each test method"""
 self.obj = MyClass()
 def teardown_method(self):
 """Run after each test method"""
 pass
 def test_basic_functionality(self):
 """Test basic usage - clear description"""
 result = self.obj.method(42)
 assert result == 84
 def test_edge_case_negative_input(self):
 """Test behavior with negative input"""
 with pytest.raises(ValueError):
 self.obj.method(-1)
 @pytest.mark.parametrize("input,expected", [
 (1, 2),
 (2, 4),
 (3, 6),
 (10, 20),
 ])
 def test_multiple_inputs(self, input, expected):
 """Test with multiple parameter values"""
 assert self.obj.method(input) == expected

Test Assertions

Common assertions:

# Equality
assert result == expected
# Approximate equality (for floats)
import math
assert math.isclose(result, expected, rel_tol=1e-5)
# NumPy arrays
import numpy as np
np.testing.assert_allclose(result, expected, rtol=1e-5)
# Value ranges
assert 0 < result < 100
# Type checks
assert isinstance(result, float)
# Exception testing
with pytest.raises(ValueError):
 function_that_should_raise()
# Exception message matching
with pytest.raises(ValueError, match="invalid input"):
 function_that_should_raise()

Fixtures

Use fixtures for reusable test data:

# conftest.py or in test file
import pytest
from dvoacap import PredictionEngine
from dvoacap.path_geometry import GeoPoint
@pytest.fixture
def prediction_engine():
 """Create a configured prediction engine"""
 engine = PredictionEngine()
 engine.params.ssn = 100.0
 engine.params.month = 6
 engine.params.tx_power = 100.0
 engine.params.tx_location = GeoPoint.from_degrees(40.0, -75.0)
 return engine
@pytest.fixture
def sample_path():
 """Create a sample propagation path"""
 tx = GeoPoint.from_degrees(40.0, -75.0)
 rx = GeoPoint.from_degrees(51.5, -0.1)
 return tx, rx
# Use fixtures in tests
def test_with_fixtures(prediction_engine, sample_path):
 """Test using fixtures"""
 tx, rx = sample_path
 engine = prediction_engine
 engine.predict(rx_location=rx, utc_time=0.5, frequencies=[14.0])
 assert len(engine.predictions) == 1

Parametrized Tests

Test multiple scenarios efficiently:

@pytest.mark.parametrize("lat,lon,expected_hemisphere", [
 (40.0, -75.0, "Northern"),
 (-33.0, 151.0, "Southern"),
 (0.0, 0.0, "Equatorial"),
])
def test_hemisphere_detection(lat, lon, expected_hemisphere):
 """Test hemisphere detection for various locations"""
 point = GeoPoint.from_degrees(lat, lon)
 hemisphere = detect_hemisphere(point)
 assert hemisphere == expected_hemisphere

Validation Testing

Comparing with Reference VOACAP

Test against reference output:

import json
from dvoacap import PredictionEngine
from dvoacap.path_geometry import GeoPoint
def test_against_voacap_reference():
 """Compare prediction with reference VOACAP output"""
 # Load reference data
 with open('SampleIO/voacap_reference/case_001.json', 'r') as f:
 reference = json.load(f)
 # Run DVOACAP prediction
 engine = PredictionEngine()
 engine.params.ssn = reference['ssn']
 engine.params.month = reference['month']
 engine.params.tx_location = GeoPoint.from_degrees(
 reference['tx_lat'], reference['tx_lon']
 )
 rx_location = GeoPoint.from_degrees(
 reference['rx_lat'], reference['rx_lon']
 )
 engine.predict(
 rx_location=rx_location,
 utc_time=reference['utc_time'],
 frequencies=[reference['frequency']]
 )
 # Compare results
 pred = engine.predictions[0]
 # Allow 10% tolerance
 assert abs(pred.signal.snr_db - reference['snr_db']) < 2.0
 assert abs(pred.signal.reliability - reference['reliability']) < 0.1

Validation Tolerances

Recommended tolerances for validation:

Parameter Tolerance Reason
Distance ±0.1 km Great circle calculation
Azimuth ±0.1° Bearing calculation
foF2 ±0.2 MHz CCIR model differences
MUF ±0.5 MHz Raytracing variations
SNR ±2.0 dB Signal model differences
Reliability ±10% Statistical variations

Coverage

Running Coverage Analysis

Generate coverage report:

# Run tests with coverage
pytest --cov=dvoacap tests/
# Sample output:
# ---------- coverage: platform linux, python 3.10.12 -----------
# Name Stmts Miss Cover
# -------------------------------------------------------
# src/dvoacap/__init__.py 12 0 100%
# src/dvoacap/path_geometry.py 156 8 95%
# src/dvoacap/ionospheric_profile.py 234 45 81%
# src/dvoacap/muf_calculator.py 189 23 88%
# src/dvoacap/prediction_engine.py 312 67 79%
# -------------------------------------------------------
# TOTAL 1234 167 86%

Generate HTML coverage report:

pytest --cov=dvoacap --cov-report=html tests/
# Open in browser
open htmlcov/index.html # macOS
xdg-open htmlcov/index.html # Linux
start htmlcov/index.html # Windows

Show missing lines:

pytest --cov=dvoacap --cov-report=term-missing tests/

Coverage Goals

Target coverage levels:

  • Overall: >80%
  • Core modules (path_geometry, ionospheric): >90%
  • Critical calculations (MUF, signal): >85%
  • Utilities and helpers: >70%

Continuous Integration

GitHub Actions

DVOACAP-Python uses GitHub Actions for CI/CD.

Validation workflow:

# .github/workflows/validation.yml
name: Validation Tests
on: [push, pull_request]
jobs:
 test:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v3
 - uses: actions/setup-python@v4
 with:
 python-version: '3.10'
 - name: Install dependencies
 run: |
 pip install -e ".[dev]"

 - name: Run tests
 run: |
 pytest -v --cov=dvoacap tests/

 - name: Run validation
 run: |
 python3 test_voacap_reference.py

View CI results:

  • Visit repository on GitHub
  • Click "Actions" tab
  • Select workflow run

Debugging Tests

Running Tests in Debug Mode

Use pdb debugger:

# Add breakpoint in test
def test_my_function():
 result = my_function(42)
 import pdb; pdb.set_trace() # Debugger will stop here
 assert result == 84

Run with debugger:

pytest tests/test_my_module.py -s
# When breakpoint hits:
# (Pdb) print(result)
# (Pdb) next # Step to next line
# (Pdb) continue # Continue execution

Print Debugging

Show print output:

pytest -s tests/test_my_module.py

Add debug output:

def test_my_function():
 result = my_function(42)
 print(f"DEBUG: result = {result}")
 assert result == 84

Failed Test Inspection

Show full traceback:

pytest -v --tb=long

Show local variables:

pytest -v --tb=short --showlocals

Best Practices

Test Naming

Use descriptive names:

# Good
def test_distance_calculation_for_transatlantic_path():
 """Test distance between Philadelphia and London"""
 pass
# Bad
def test1():
 pass

Test Independence

Each test should be independent:

# Good - each test sets up its own data
def test_feature_a():
 data = create_test_data()
 assert feature_a(data) == expected
def test_feature_b():
 data = create_test_data()
 assert feature_b(data) == expected
# Bad - tests depend on each other
def test_setup():
 global data
 data = create_test_data()
def test_feature():
 assert feature(data) == expected # Depends on test_setup

Fast Tests

Keep tests fast:

  • Mock expensive operations
  • Use small datasets
  • Avoid network calls
  • Target: <10 seconds for full test suite

Test Documentation

Use clear docstrings:

def test_muf_calculation_at_solar_maximum():
 """
 Test MUF calculation during solar maximum conditions.

 Verifies that MUF is calculated correctly when SSN=200,
 matching reference VOACAP output within 0.5 MHz tolerance.
 """
 pass

Next Steps


Need help? Check Troubleshooting or open an issue on GitHub.

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /