Testing
Overview
JAFF uses pytest for testing. This guide covers how to write, run, and organize tests.
Running Tests
Basic Commands
# Run all tests
pytest
# Run with verbose output
pytest -v
# Run specific test file
pytest tests/test_network.py
# Run specific test
pytest tests/test_network.py::test_load_network
# Run tests matching pattern
pytest -k "network"
Coverage
# Run with coverage
pytest --cov=jaff
# Generate HTML coverage report
pytest --cov=jaff --cov-report=html
# View report
open htmlcov/index.html
Markers
NOTE: Markers have not yet been added
# Run only fast tests
pytest -m "not slow"
# Run only slow tests
pytest -m slow
# Run only unit tests
pytest -m unit
Test Organization
Directory Structure
tests/
├── __init__.py
├── conftest.py # Shared fixtures
├── test_network.py # Network class tests
├── test_codegen.py # Codegen class tests
├── test_file_parser.py # File parser tests
├── test_elements.py # Elements class tests
├── test_cli.py # CLI tests
└── fixtures/ # Test data
├── networks/
│ ├── test_small.dat
│ └── test_krome.dat
└── templates/
└── test_template.cpp
NOTE:
networksandtemplatesfolder bifurcation will be done once templating tests are added to the test suite
Test File Structure
"""Tests for Network class."""
import pytest
from jaff import Network
class TestNetwork:
"""Network class tests."""
def test_load_basic_network(self):
"""Test loading basic network."""
net = Network("tests/fixtures/networks/test_small.dat")
assert len(net.species) > 0
assert len(net.reactions) > 0
def test_load_krome_format(self):
"""Test loading KROME format."""
net = Network("tests/fixtures/networks/test_krome.dat")
assert net.label is not None
class TestNetworkValidation:
"""Network validation tests."""
def test_duplicate_reactions(self):
"""Test detection of duplicate reactions."""
# Test implementation
pass
Writing Tests
Basic Test
def test_network_species_count():
"""Test species count is correct."""
net = Network("tests/fixtures/networks/test.dat")
assert len(net.species) == 35
assert net.get_number_of_species() == 35
Testing Exceptions
def test_invalid_network_raises_error():
"""Test that invalid network raises error."""
with pytest.raises(FileNotFoundError):
Network("nonexistent.dat")
def test_negative_temperature_raises_error():
"""Test negative temperature raises ValueError."""
net = Network("tests/fixtures/networks/test.dat")
with pytest.raises(ValueError, match="Temperature must be positive"):
net.calculate_rates(-100)
Parametrized Tests
@pytest.mark.parametrize("lang,expected_bracket", [
("c++", "["),
("c", "["),
("fortran", "("),
("python", "["),
])
def test_codegen_brackets(lang, expected_bracket):
"""Test bracket style for each language."""
net = Network("tests/fixtures/networks/test.dat")
cg = Codegen(network=net, lang=lang)
assert cg.lb == expected_bracket
Parametrized with Multiple Arguments
@pytest.mark.parametrize("filename,expected_species,expected_reactions", [
("test_small.dat", 10, 20),
("test_medium.dat", 50, 100),
("test_large.dat", 200, 500),
])
def test_network_sizes(filename, expected_species, expected_reactions):
"""Test networks load with expected sizes."""
net = Network(f"tests/fixtures/networks/{filename}")
assert len(net.species) == expected_species
assert len(net.reactions) == expected_reactions
Fixtures
Basic Fixtures
# conftest.py
import pytest
from jaff import Network
@pytest.fixture
def small_network():
"""Small test network."""
return Network("tests/fixtures/networks/test_small.dat")
@pytest.fixture
def codegen_cpp(small_network):
"""C++ code generator for small network."""
from jaff import Codegen
return Codegen(network=small_network, lang="c++")
Using Fixtures
def test_with_fixture(small_network):
"""Test using network fixture."""
assert len(small_network.species) > 0
def test_code_generation(codegen_cpp):
"""Test code generation."""
rates = codegen_cpp.get_rates(use_cse=True)
assert "k[0]" in rates
Fixture Scope
@pytest.fixture(scope="module")
def expensive_network():
"""Module-scoped fixture for expensive setup."""
# Loaded once per module
return Network("tests/fixtures/networks/large.dat")
@pytest.fixture(scope="function")
def temp_directory(tmp_path):
"""Function-scoped temporary directory."""
# New directory for each test
return tmp_path
Test Data
Creating Test Networks
# tests/fixtures/networks/test_small.dat
H + O -> OH, 1.2e-10 * (tgas/300)**0.5
H2 + O -> OH + H, 3.4e-11 * exp(-500/tgas)
C + O2 -> CO + O, 5.6e-12
Using Temporary Files
def test_network_save_load(tmp_path):
"""Test saving and loading network."""
# Create network
net = Network("tests/fixtures/networks/test.dat")
# Save to temporary file
output_file = tmp_path / "test.jaff"
net.to_jaff_file(str(output_file))
# Load and verify
net2 = Network(str(output_file))
assert len(net2.species) == len(net.species)
Mocking
Mock External Dependencies
from unittest.mock import Mock, patch
def test_with_mock():
"""Test with mocked function."""
with patch('jaff.network.open') as mock_open:
mock_open.return_value.__enter__.return_value.readlines.return_value = [
"H + O -> OH, 1.2e-10\n"
]
# Test code
Mock Network Loading
@pytest.fixture
def mock_network():
"""Mock network for testing."""
net = Mock(spec=Network)
net.species = [Mock(name="H"), Mock(name="O")]
net.reactions = [Mock()]
return net
def test_with_mock_network(mock_network):
"""Test using mocked network."""
assert len(mock_network.species) == 2
Testing Best Practices
1. Test One Thing at a Time
# Good
def test_network_loads_successfully():
"""Test network loads without error."""
net = Network("test.dat")
assert net is not None
def test_network_has_species():
"""Test network has species."""
net = Network("test.dat")
assert len(net.species) > 0
# Avoid
def test_everything():
"""Test everything at once."""
net = Network("test.dat")
assert net is not None
assert len(net.species) > 0
# ... many more assertions
2. Use Descriptive Names
# Good
def test_codegen_raises_error_for_unsupported_language():
"""Test that unsupported language raises ValueError."""
pass
# Avoid
def test_lang():
pass
3. Test Edge Cases
def test_empty_network():
"""Test handling of empty network."""
pass
def test_network_with_one_species():
"""Test network with single species."""
pass
def test_network_with_duplicate_species():
"""Test handling of duplicate species."""
pass
4. Test Error Conditions
def test_file_not_found():
"""Test FileNotFoundError for missing file."""
with pytest.raises(FileNotFoundError):
Network("nonexistent.dat")
def test_invalid_format():
"""Test ValueError for invalid format."""
with pytest.raises(ValueError):
Network("tests/fixtures/invalid.dat")
Integration Tests
Testing Complete Workflows
def test_complete_code_generation_workflow(tmp_path):
"""Test complete workflow from network to code."""
# Load network
net = Network("tests/fixtures/networks/test.dat")
# Create code generator
cg = Codegen(network=net, lang="c++")
# Generate code
rates = cg.get_rates(use_cse=True)
odes = cg.get_ode(use_cse=True)
# Save to file
output_file = tmp_path / "chemistry.cpp"
with open(output_file, 'w') as f:
f.write(rates)
f.write("\n\n")
f.write(odes)
# Verify file exists and has content
assert output_file.exists()
content = output_file.read_text()
assert "k[0]" in content
assert "dydt[0]" in content
Performance Tests
Timing Tests
import time
@pytest.mark.slow
def test_large_network_performance():
"""Test large network loads in reasonable time."""
start = time.time()
net = Network("tests/fixtures/networks/large.dat")
duration = time.time() - start
assert duration < 30.0 # Should load in < 30 seconds
Memory Tests
import tracemalloc
@pytest.mark.slow
def test_memory_usage():
"""Test memory usage is reasonable."""
tracemalloc.start()
net = Network("tests/fixtures/networks/test.dat")
cg = Codegen(network=net, lang="c++")
rates = cg.get_rates(use_cse=True)
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
# Peak memory should be reasonable
assert peak < 100 * 1024 * 1024 # < 100 MB
Continuous Integration
GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, "3.10", 3.11]
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -e ".[dev]"
- name: Run tests
run: |
pytest --cov=jaff --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
Coverage Goals
- Overall coverage: > 80%
- Critical modules: > 90%
- New code: 100% coverage required
Check Coverage
# Generate coverage report
pytest --cov=jaff --cov-report=term-missing
# Show lines not covered
pytest --cov=jaff --cov-report=html
open htmlcov/index.html
Troubleshooting Tests
Test Failures
# Run with detailed output
pytest -vv
# Show print statements
pytest -s
# Stop at first failure
pytest -x
# Run last failed tests
pytest --lf
Debugging Tests
def test_debug_example():
"""Test with debugging."""
net = Network("test.dat")
# Add breakpoint
import pdb; pdb.set_trace()
# Or use pytest's built-in
pytest.set_trace()
assert len(net.species) > 0
Test Markers
Defining Markers
# pytest.ini or pyproject.toml
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow",
"unit: unit tests",
"integration: integration tests",
"network: network-related tests",
]
Using Markers
@pytest.mark.slow
def test_large_network():
"""Slow test."""
pass
@pytest.mark.unit
def test_species_creation():
"""Unit test."""
pass
@pytest.mark.integration
@pytest.mark.network
def test_full_workflow():
"""Integration test for network workflow."""
pass