LIBRE / tests /unit /test_entities.py
RyZ
feat: adding full working local ETL Pipeline
e391a84
Raw
History Blame Contribute Delete
7.53 kB
"""
tests/unit/test_entities.py
────────────────────────────
Unit tests for domain entities: PPGSignal and BPPrediction.
These tests require ZERO external dependencies (no DB, no broker, no HTTP).
They validate pure business rules and domain validation logic.
"""
from __future__ import annotations
import pytest
from src.domain.entities.ppg_signal import PPGSignal
from src.domain.entities.prediction import BPPrediction
from src.domain.exceptions.domain_exceptions import InvalidSignalError, PredictionOutOfRangeError
# ── Fixtures ──────────────────────────────────────────────────────────────────
def make_signal(**overrides) -> PPGSignal:
"""Build a valid PPGSignal with optional field overrides."""
defaults = {
"device_id": "sensor-001",
"user_id": "user-123",
"sampling_rate": 125.0,
"ppg_values": [0.1] * 1250, # 10 seconds Γ— 125 Hz = 1250 samples
"duration_seconds": 10.0,
}
defaults.update(overrides)
return PPGSignal(**defaults)
def make_prediction(**overrides) -> BPPrediction:
"""Build a valid BPPrediction with optional field overrides."""
defaults = {
"ppg_signal_id": "test-signal-id",
"predicted_sbp": 120.0,
"predicted_dbp": 80.0,
"model_version": "test-v1",
"inference_time_ms": 100.0,
}
defaults.update(overrides)
return BPPrediction(**defaults)
# ── PPGSignal Tests ───────────────────────────────────────────────────────────
class TestPPGSignalValidation:
def test_valid_signal_passes_validation(self) -> None:
signal = make_signal()
signal.validate() # should not raise
def test_empty_device_id_raises(self) -> None:
with pytest.raises(InvalidSignalError) as exc_info:
make_signal(device_id="").validate()
assert exc_info.value.field == "device_id"
def test_empty_user_id_raises(self) -> None:
with pytest.raises(InvalidSignalError) as exc_info:
make_signal(user_id=" ").validate()
assert exc_info.value.field == "user_id"
def test_sampling_rate_too_low_raises(self) -> None:
with pytest.raises(InvalidSignalError) as exc_info:
make_signal(sampling_rate=10.0, ppg_values=[0.1] * 100, duration_seconds=10.0).validate()
assert exc_info.value.field == "sampling_rate"
def test_sampling_rate_too_high_raises(self) -> None:
with pytest.raises(InvalidSignalError) as exc_info:
make_signal(
sampling_rate=2000.0,
ppg_values=[0.1] * 20000,
duration_seconds=10.0,
).validate()
assert exc_info.value.field == "sampling_rate"
def test_empty_ppg_values_raises(self) -> None:
with pytest.raises(InvalidSignalError) as exc_info:
make_signal(ppg_values=[]).validate()
assert exc_info.value.field == "ppg_values"
def test_zero_duration_raises(self) -> None:
# duration_seconds=0 β†’ also fails sample count consistency
with pytest.raises(InvalidSignalError):
make_signal(duration_seconds=0.0, ppg_values=[0.1] * 10).validate()
def test_sample_count_inconsistency_raises(self) -> None:
# 1 second at 125 Hz β†’ expect 125 samples, give 500
with pytest.raises(InvalidSignalError) as exc_info:
make_signal(
duration_seconds=1.0,
sampling_rate=125.0,
ppg_values=[0.1] * 500,
).validate()
assert exc_info.value.field == "ppg_values"
def test_sample_count_within_tolerance_passes(self) -> None:
# 10% tolerance: 125 samples expected, 130 is within range
signal = make_signal(
duration_seconds=1.0,
sampling_rate=125.0,
ppg_values=[0.1] * 130,
)
signal.validate() # should not raise
class TestPPGSignalProperties:
def test_num_samples(self) -> None:
signal = make_signal(ppg_values=[0.1] * 500)
assert signal.num_samples == 500
def test_mean_value(self) -> None:
signal = make_signal(ppg_values=[1.0, 2.0, 3.0] * 500)
assert abs(signal.mean_value - 2.0) < 1e-6
def test_is_valid_true_for_valid_signal(self) -> None:
assert make_signal().is_valid is True
def test_is_valid_false_for_invalid_signal(self) -> None:
assert make_signal(device_id="").is_valid is False
def test_to_dict_round_trip(self) -> None:
signal = make_signal()
d = signal.to_dict()
restored = PPGSignal.from_dict(d)
assert restored.device_id == signal.device_id
assert restored.user_id == signal.user_id
assert restored.sampling_rate == signal.sampling_rate
assert restored.ppg_values == signal.ppg_values
# ── BPPrediction Tests ────────────────────────────────────────────────────────
class TestBPPredictionValidation:
def test_valid_prediction_passes(self) -> None:
make_prediction().validate() # should not raise
def test_sbp_too_low_raises(self) -> None:
with pytest.raises(PredictionOutOfRangeError):
make_prediction(predicted_sbp=50.0).validate()
def test_sbp_too_high_raises(self) -> None:
with pytest.raises(PredictionOutOfRangeError):
make_prediction(predicted_sbp=270.0).validate()
def test_dbp_too_low_raises(self) -> None:
with pytest.raises(PredictionOutOfRangeError):
make_prediction(predicted_dbp=20.0).validate()
def test_dbp_too_high_raises(self) -> None:
with pytest.raises(PredictionOutOfRangeError):
make_prediction(predicted_dbp=165.0).validate()
def test_sbp_less_than_dbp_raises(self) -> None:
with pytest.raises(PredictionOutOfRangeError):
make_prediction(predicted_sbp=70.0, predicted_dbp=80.0).validate()
class TestBPPredictionProperties:
def test_mean_arterial_pressure(self) -> None:
p = make_prediction(predicted_sbp=120.0, predicted_dbp=80.0)
# MAP = 80 + (120 - 80) / 3 = 80 + 13.33 = 93.33
assert abs(p.mean_arterial_pressure - 93.33) < 0.1
def test_pulse_pressure(self) -> None:
p = make_prediction(predicted_sbp=120.0, predicted_dbp=80.0)
assert p.pulse_pressure == pytest.approx(40.0)
def test_hypertension_stage_normal(self) -> None:
assert make_prediction(predicted_sbp=115.0, predicted_dbp=75.0).hypertension_stage == "Normal"
def test_hypertension_stage_elevated(self) -> None:
assert make_prediction(predicted_sbp=125.0, predicted_dbp=75.0).hypertension_stage == "Elevated"
def test_hypertension_stage_stage1(self) -> None:
assert make_prediction(predicted_sbp=132.0, predicted_dbp=82.0).hypertension_stage == "Stage 1"
def test_hypertension_stage_stage2(self) -> None:
assert make_prediction(predicted_sbp=145.0, predicted_dbp=92.0).hypertension_stage == "Stage 2"
def test_hypertension_stage_crisis(self) -> None:
assert make_prediction(predicted_sbp=185.0, predicted_dbp=125.0).hypertension_stage == "Crisis"