This guide covers the testing framework, practices, and standards for the PyBiorythm project.
The project uses a comprehensive testing strategy with:
tests/
├── conftest.py # Shared fixtures and configuration
├── test_biorhythm_calculator.py # Core functionality tests
├── test_main.py # CLI interface tests
├── test_json_timeseries.py # JSON output tests
└── test_coverage_gaps.py # Edge case coverage
# Activate virtual environment
source .venv/bin/activate
# Run all tests
uv run pytest
# Run with coverage report
uv run pytest --cov=. --cov-report=term-missing
# Run with coverage requirement enforcement
uv run pytest --cov=. --cov-fail-under=85
# Generate HTML coverage report
uv run pytest --cov=. --cov-report=html
# View in htmlcov/index.html
# Run specific test file
uv run pytest tests/test_biorhythm_calculator.py
# Run specific test class
uv run pytest tests/test_biorhythm_calculator.py::TestBiorhythmCalculator
# Run specific test method
uv run pytest tests/test_main.py::TestMainFunction::test_interactive_mode
# Run tests matching pattern
uv run pytest -k "json" -v
# Run only fast tests (exclude slow benchmarks)
uv run pytest -m "not slow"
Tests run automatically on:
main
or develop
branchesmain
Test individual functions and methods in isolation.
import pytest
from datetime import datetime
from biorythm.core import BiorhythmCalculator, DateValidationError
class TestBiorhythmCalculator:
def test_calculate_biorhythm_values(self):
"""Test biorhythm value calculations"""
calc = BiorhythmCalculator()
birthdate = datetime(1990, 5, 15)
target_date = datetime(1990, 5, 15) # Same day = all zeros
physical, emotional, intellectual = calc.calculate_biorhythm_values(
birthdate, target_date
)
# Values should be at zero on birth date
assert abs(physical) < 0.01
assert abs(emotional) < 0.01
assert abs(intellectual) < 0.01
def test_critical_day_detection(self):
"""Test critical day identification"""
calc = BiorhythmCalculator()
# Test critical day (values near zero)
is_critical, cycles = calc.is_critical_day(0.02, -0.03, 0.5)
assert is_critical is True
assert "Physical" in cycles
assert "Emotional" in cycles
assert "Intellectual" not in cycles
Test component interactions and full workflows.
class TestChartGeneration:
def test_vertical_chart_generation(self, capsys):
"""Test complete vertical chart generation"""
calc = BiorhythmCalculator(width=30, days=7, orientation="vertical")
birthdate = datetime(1990, 5, 15)
calc.generate_chart(birthdate)
captured = capsys.readouterr()
assert "BIORHYTHM CHART (VERTICAL)" in captured.out
assert "Physical (23-day cycle)" in captured.out
assert "CRITICAL DAYS" in captured.out or "No critical days" in captured.out
def test_json_output_structure(self):
"""Test JSON output format and content"""
calc = BiorhythmCalculator(days=5)
birthdate = datetime(1990, 5, 15)
data = calc.generate_timeseries_json(birthdate)
# Validate structure
assert "meta" in data
assert "data" in data
assert "critical_days" in data
assert "cycle_repeats" in data
# Validate metadata
assert data["meta"]["birthdate"] == "1990-05-15"
assert len(data["data"]) == 5
# Validate data entries
for entry in data["data"]:
assert "date" in entry
assert "physical" in entry
assert -1.0 <= entry["physical"] <= 1.0
Test exception cases and error conditions.
class TestErrorHandling:
def test_invalid_date_validation(self):
"""Test date validation errors"""
from biorythm.core import DateValidator
with pytest.raises(DateValidationError):
DateValidator.create_validated_date(2030, 5, 15) # Future date
with pytest.raises(DateValidationError):
DateValidator.create_validated_date(1990, 13, 15) # Invalid month
with pytest.raises(DateValidationError):
DateValidator.create_validated_date(1990, 2, 30) # Invalid day
def test_chart_parameter_validation(self):
"""Test chart parameter validation"""
from biorythm.core import ChartParameterError
with pytest.raises(ChartParameterError):
BiorhythmCalculator(width=-1) # Negative width
with pytest.raises(ChartParameterError):
BiorhythmCalculator(days=0) # Zero days
with pytest.raises(ChartParameterError):
BiorhythmCalculator(orientation="invalid") # Invalid orientation
Benchmark critical operations for regression testing.
import pytest
class TestPerformance:
@pytest.mark.benchmark
def test_calculation_performance(self, benchmark):
"""Benchmark biorhythm calculation performance"""
calc = BiorhythmCalculator()
birthdate = datetime(1990, 5, 15)
target_date = datetime.now()
result = benchmark(calc.calculate_biorhythm_values, birthdate, target_date)
# Ensure calculation is fast enough
assert len(result) == 3 # Returns three values
@pytest.mark.benchmark
def test_chart_generation_performance(self, benchmark):
"""Benchmark chart generation performance"""
calc = BiorhythmCalculator(days=30)
birthdate = datetime(1990, 5, 15)
# Should complete within reasonable time
benchmark(calc.generate_chart, birthdate)
@pytest.mark.slow
def test_large_dataset_performance(self):
"""Test performance with large datasets"""
calc = BiorhythmCalculator(days=365) # Full year
birthdate = datetime(1990, 5, 15)
import time
start_time = time.time()
data = calc.generate_timeseries_json(birthdate)
elapsed = time.time() - start_time
# Should process year of data quickly
assert elapsed < 5.0 # Less than 5 seconds
assert len(data["data"]) == 365
[tool:pytest]
minversion = 6.0
addopts =
-ra
--strict-markers
--strict-config
--cov=biorythm
--cov-report=term-missing:skip-covered
--cov-fail-under=85
testpaths = tests
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
benchmark: marks tests as performance benchmarks
import pytest
from datetime import datetime
from biorythm.core import BiorhythmCalculator
@pytest.fixture
def sample_birthdate():
"""Standard birthdate for testing"""
return datetime(1990, 5, 15)
@pytest.fixture
def calculator():
"""Standard calculator instance"""
return BiorhythmCalculator(width=30, days=7)
@pytest.fixture
def calculator_horizontal():
"""Horizontal orientation calculator"""
return BiorhythmCalculator(width=30, days=7, orientation="horizontal")
@pytest.fixture
def mock_current_date():
"""Mock current date for consistent testing"""
return datetime(2025, 8, 7)
# Configure markers
def pytest_configure(config):
config.addinivalue_line("markers", "slow: mark test as slow running")
config.addinivalue_line("markers", "benchmark: mark test as performance benchmark")
All code must maintain at least 85% test coverage:
# Check current coverage
uv run pytest --cov=. --cov-report=term-missing --cov-fail-under=85
# View detailed coverage report
uv run pytest --cov=. --cov-report=html
open htmlcov/index.html
Some code is excluded from coverage requirements:
# pyproject.toml
[tool.coverage.run]
omit = [
"_version.py",
"*/tests/*",
"*/venv/*",
"*/.venv/*"
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
]
Use consistent test data for reproducible results:
# Standard test cases
SAMPLE_BIRTHDATES = [
datetime(1990, 5, 15), # Standard case
datetime(1980, 1, 1), # New year birth
datetime(2000, 2, 29), # Leap year birth
datetime(1995, 12, 31), # Year-end birth
]
SAMPLE_DATES = [
datetime(2025, 8, 7), # Current test date
datetime(2025, 1, 1), # Year start
datetime(2025, 12, 31), # Year end
]
@pytest.fixture
def biorhythm_timeseries_data():
"""Generate sample timeseries data"""
calc = BiorhythmCalculator(days=14)
birthdate = datetime(1990, 5, 15)
return calc.generate_timeseries_json(birthdate)
@pytest.fixture
def critical_day_scenario():
"""Create scenario with known critical days"""
# Calculate specific dates that will be critical
calc = BiorhythmCalculator(days=60)
birthdate = datetime(1990, 1, 1)
data = calc.generate_timeseries_json(birthdate)
# Return data with guaranteed critical days
return [entry for entry in data["data"] if entry["critical_cycles"]]
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: $
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python $
uses: actions/setup-python@v5
with:
python-version: $
- name: Install UV
uses: astral-sh/setup-uv@v6
- name: Install dependencies
run: uv sync --group dev
- name: Run tests with coverage
run: uv run pytest --cov=. --cov-report=xml --cov-fail-under=85
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
class TestBiorhythmCalculation:
"""Tests for core biorhythm calculation functionality"""
def test_physical_cycle_calculation_on_birth_date(self):
"""Physical cycle should be zero on birth date"""
# Arrange
calc = BiorhythmCalculator()
birthdate = datetime(1990, 5, 15)
# Act
physical, _, _ = calc.calculate_biorhythm_values(birthdate, birthdate)
# Assert
assert abs(physical) < 0.01, "Physical cycle should be near zero on birth date"
def test_cycle_values_within_valid_range(self):
"""All cycle values should be between -1 and 1"""
# Arrange
calc = BiorhythmCalculator()
birthdate = datetime(1990, 5, 15)
test_date = datetime(2025, 8, 7)
# Act
physical, emotional, intellectual = calc.calculate_biorhythm_values(
birthdate, test_date
)
# Assert
assert -1 <= physical <= 1, f"Physical value {physical} out of range"
assert -1 <= emotional <= 1, f"Emotional value {emotional} out of range"
assert -1 <= intellectual <= 1, f"Intellectual value {intellectual} out of range"
from unittest.mock import patch, mock_open
import pytest
class TestFileOperations:
@patch("builtins.open", new_callable=mock_open)
@patch("json.dump")
def test_json_export(self, mock_json_dump, mock_file):
"""Test JSON file export functionality"""
calc = BiorhythmCalculator()
data = {"test": "data"}
# Test the export (if such functionality existed)
# calc.export_json(data, "test.json")
mock_file.assert_called_once_with("test.json", "w")
mock_json_dump.assert_called_once()
@patch("datetime.datetime")
def test_current_date_calculation(self, mock_datetime):
"""Test calculation using mocked current date"""
# Mock datetime.now() to return specific date
mock_datetime.now.return_value = datetime(2025, 8, 7, 12, 0, 0)
mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw)
calc = BiorhythmCalculator()
# Test functionality that depends on current date
# Run with maximum verbosity
uv run pytest -vvv
# Show local variables on failure
uv run pytest -l
# Drop into debugger on failure
uv run pytest --pdb
# Run specific test with debugging
uv run pytest tests/test_main.py::test_specific_function -vvv --pdb
import logging
def test_with_logging(caplog):
"""Test with log capture"""
with caplog.at_level(logging.INFO):
calc = BiorhythmCalculator()
# ... test code ...
# Check log messages
assert "BiorhythmCalculator initialized" in caplog.text
assert caplog.records[0].levelname == "INFO"
import tempfile
import json
def test_json_file_operations(tmp_path):
"""Test operations with temporary files"""
# Create temporary file
test_file = tmp_path / "test_data.json"
# Generate and save data
calc = BiorhythmCalculator(days=5)
data = calc.generate_timeseries_json(datetime(1990, 5, 15))
with open(test_file, "w") as f:
json.dump(data, f)
# Verify file contents
with open(test_file, "r") as f:
loaded_data = json.load(f)
assert loaded_data["meta"]["days"] == 5
# Run benchmarks and save results
uv run pytest --benchmark-only --benchmark-json=benchmark.json
# Compare benchmark results
uv run pytest --benchmark-compare=previous_benchmark.json
# Set performance thresholds
uv run pytest --benchmark-max-time=2.0
import psutil
import os
def test_memory_usage():
"""Monitor memory usage during large operations"""
process = psutil.Process(os.getpid())
initial_memory = process.memory_info().rss
# Perform memory-intensive operation
calc = BiorhythmCalculator(days=1000)
data = calc.generate_timeseries_json(datetime(1990, 5, 15))
final_memory = process.memory_info().rss
memory_increase = (final_memory - initial_memory) / 1024 / 1024 # MB
# Assert memory usage is reasonable
assert memory_increase < 50, f"Memory usage increased by {memory_increase:.1f}MB"
def test_scientific_warnings_present():
"""Ensure scientific disclaimers are present in outputs"""
calc = BiorhythmCalculator()
data = calc.generate_timeseries_json(datetime(1990, 5, 15))
warning = data["meta"]["scientific_warning"]
assert "PSEUDOSCIENCE" in warning
assert "NO scientific evidence" in warning
assert "ENTERTAINMENT PURPOSES ONLY" in warning
import math
def test_cycle_mathematical_accuracy():
"""Verify mathematical correctness of cycle calculations"""
calc = BiorhythmCalculator()
birthdate = datetime(1990, 5, 15)
# Test known mathematical properties
# Physical cycle (23 days) should repeat after 23 days
base_date = datetime(2025, 8, 7)
cycle_date = base_date + timedelta(days=23)
p1, _, _ = calc.calculate_biorhythm_values(birthdate, base_date)
p2, _, _ = calc.calculate_biorhythm_values(birthdate, cycle_date)
# Values should be very close (accounting for floating-point precision)
assert abs(p1 - p2) < 0.001, "23-day physical cycle should repeat"
def test_cycle_amplitude_bounds():
"""Verify cycle values never exceed mathematical bounds"""
calc = BiorhythmCalculator()
birthdate = datetime(1970, 1, 1) # Long time span
# Test many dates to ensure bounds
test_dates = [datetime(1970, 1, 1) + timedelta(days=i) for i in range(0, 10000, 100)]
for test_date in test_dates:
p, e, i = calc.calculate_biorhythm_values(birthdate, test_date)
# Sine function bounds: -1 ≤ sin(x) ≤ 1
assert -1 <= p <= 1, f"Physical {p} exceeds bounds on {test_date}"
assert -1 <= e <= 1, f"Emotional {e} exceeds bounds on {test_date}"
assert -1 <= i <= 1, f"Intellectual {i} exceeds bounds on {test_date}"
--cov-report=term-missing
# Clean test environment
uv sync --group dev # Reinstall dependencies
rm -rf .pytest_cache # Clear pytest cache
rm -rf htmlcov/ # Remove old coverage reports
# Reset UV environment
uv clean # Clear UV cache
rm -rf .venv # Remove virtual environment
uv sync --group dev # Recreate environment