Testing Guide¶
How to run, write, and maintain tests for the locomotion data system.
Running Tests¶
Quick Test¶
# Run core tests only (fast)
pytest tests/test_locomotion_data_library.py -v
# Run with coverage
pytest tests/ --cov=lib --cov-report=html
# Open htmlcov/index.html to view coverage report
Full Test Suite¶
# All tests with verbose output
pytest tests/ -v
# Parallel execution (faster)
pytest tests/ -n auto
# Stop on first failure (debugging)
pytest tests/ -x
Specific Tests¶
# Run tests matching a pattern
pytest tests/ -k "validation"
# Run a specific test function
pytest tests/test_validation_parser.py::test_parse_kinematic_spec
# Run with debugging output
pytest tests/ -s # Shows print statements
Test Organization¶
tests/
├── test_*.py # Unit tests
├── demo_*.py # Visual demonstrations
├── test_data/ # Test datasets
│ ├── demo_clean_phase.parquet # Valid data
│ └── demo_violations_phase.parquet # Invalid data
└── sample_plots/ # Expected output images
Writing Tests¶
Basic Test Structure¶
# tests/test_new_feature.py
import pytest
import numpy as np
from lib.core.locomotion_analysis import LocomotionData
class TestNewFeature:
"""Test suite for new feature."""
def setup_method(self):
"""Run before each test."""
self.data = LocomotionData('tests/test_data/demo_clean_phase.parquet')
def test_feature_basic(self):
"""Test basic functionality."""
result = self.data.new_feature()
assert result is not None
assert len(result) > 0
def test_feature_edge_case(self):
"""Test edge cases."""
with pytest.raises(ValueError):
self.data.new_feature(invalid_param=-1)
Testing Data Processing¶
def test_data_transformation():
"""Test data transformation preserves shape."""
# Arrange
original_data = np.random.randn(10, 150, 20)
# Act
transformed = transform_function(original_data)
# Assert
assert transformed.shape == original_data.shape
assert not np.isnan(transformed).any()
assert np.allclose(transformed.mean(), 0, atol=0.1)
Testing Validation¶
def test_validation_catches_errors():
"""Test validator identifies known issues."""
# Use data with known problems
validator = PhaseValidator()
results = validator.validate('tests/test_data/demo_violations_phase.parquet')
# Should fail validation
assert not results['is_valid']
assert 'knee_flexion_angle_ipsi_rad' in results['failed_variables']
assert results['num_invalid_strides'] > 0
Testing File I/O¶
def test_parquet_round_trip(tmp_path):
"""Test saving and loading preserves data."""
# Create test data
original = create_test_dataset()
# Save to temporary file
output_file = tmp_path / "test.parquet"
original.to_parquet(output_file)
# Load and compare
loaded = pd.read_parquet(output_file)
pd.testing.assert_frame_equal(original, loaded)
Test Fixtures¶
Shared Test Data¶
# tests/conftest.py
import pytest
from lib.core.locomotion_analysis import LocomotionData
@pytest.fixture
def sample_data():
"""Provide sample dataset for tests."""
return LocomotionData('tests/test_data/demo_clean_phase.parquet')
@pytest.fixture
def invalid_data():
"""Provide dataset with known issues."""
return LocomotionData('tests/test_data/demo_violations_phase.parquet')
# Use in tests:
def test_with_fixture(sample_data):
assert sample_data.num_cycles > 0
Testing Best Practices¶
1. Test One Thing¶
# Good: Focused test
def test_variable_name_validation():
"""Test that invalid variable names raise error."""
with pytest.raises(ValueError, match="not in standard variables"):
validate_variable_name("invalid_name")
# Bad: Testing multiple things
def test_everything():
"""Test all the things.""" # Too broad!
2. Use Descriptive Names¶
# Good
def test_knee_angle_stays_within_physiological_range():
# Bad
def test_1():
3. Test Edge Cases¶
def test_empty_dataset():
"""Test behavior with empty data."""
def test_single_stride():
"""Test with minimum valid data."""
def test_missing_columns():
"""Test graceful handling of incomplete data."""
4. Use Assertions Wisely¶
# Good: Specific assertion with message
assert result > 0, f"Expected positive value, got {result}"
# Better: Use pytest.approx for floats
assert result == pytest.approx(expected, rel=1e-3)
# Best: Multiple related assertions
assert result.shape == (10, 150, 20)
assert result.dtype == np.float64
assert not np.isnan(result).any()
Continuous Integration¶
Tests run automatically on: - Every push to main branch - Every pull request - Nightly for full validation suite
CI Configuration¶
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- run: pip install -r requirements.txt
- run: pytest tests/ --cov=lib
Performance Testing¶
Benchmarking¶
# tests/test_performance.py
import time
def test_loading_performance(benchmark):
"""Test data loading stays fast."""
def load_data():
return LocomotionData('large_dataset.parquet')
# Should complete in < 1 second
result = benchmark(load_data)
assert benchmark.stats['mean'] < 1.0
Memory Testing¶
import tracemalloc
def test_memory_usage():
"""Test memory usage stays reasonable."""
tracemalloc.start()
# Load large dataset
data = LocomotionData('large_dataset.parquet')
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
# Should use < 1GB for typical dataset
assert peak / 1024 / 1024 < 1000 # MB
Debugging Failed Tests¶
Get More Information¶
# Show local variables on failure
pytest tests/ --showlocals
# Drop into debugger on failure
pytest tests/ --pdb
# Show full diff for assertions
pytest tests/ -vv
Common Issues¶
Problem | Solution |
---|---|
Import errors | Check PYTHONPATH, activate venv |
File not found | Use absolute paths or pytest fixtures |
Floating point comparison | Use pytest.approx() |
Random failures | Set random seed, check for race conditions |
Slow tests | Use pytest-xdist for parallel execution |