relo-calculator / tests /test_model.py
sabilmakbar's picture
raise test coverage to 100% (152 tests)
5d65650
Raw
History Blame Contribute Delete
4.19 kB
"""Tests for the savings model and the savings-target salary solver."""
import pytest
from app.model import (
W_RENT,
W_NON_RENT,
calculate_stats,
required_net_new_for_savings_increase,
)
HIGHER = {"valuePct": 134.5, "direction": "higher"}
RENT_HIGHER = {"valuePct": 409.4, "direction": "higher"}
LOWER = {"valuePct": -12.0, "direction": "lower"}
RENT_LOWER = {"valuePct": -25.5, "direction": "lower"}
def test_home_buckets_use_fixed_weights():
m = calculate_stats(10000, 12000, HIGHER, RENT_HIGHER)
assert m["rent_home"] == pytest.approx(W_RENT * 10000)
assert m["non_rent_home"] == pytest.approx(W_NON_RENT * 10000)
# savings = remainder (20%)
assert m["savings_home"] == pytest.approx(10000 * (1 - W_RENT - W_NON_RENT))
def test_same_currency_no_fx():
m = calculate_stats(10000, 10000, HIGHER, RENT_HIGHER, fx_rate=1.0)
# With identical salary and higher costs, dest savings must be lower.
assert m["savings_new"] < m["savings_home"]
assert m["fx_rate"] == 1.0
# home-equiv equals raw when no FX
assert m["savings_new_home_equiv"] == pytest.approx(m["savings_new"])
def test_fx_scaling_keeps_costs_sane():
# Regression for the original currency-mismatch bug: destination rent must
# be on the destination-currency scale, not the home-currency scale.
fx = 0.30
m = calculate_stats(16000, 24000, HIGHER, RENT_HIGHER, fx_rate=fx)
net_home_dest = 16000 * fx
assert m["rent_new"] == pytest.approx(W_RENT * net_home_dest * (1 + 4.094))
# Savings should be a plausible positive number, not hugely negative.
assert m["savings_new"] > 0
def test_savings_pct_delta_sign():
# Cheaper destination + same salary => more savings => positive delta.
m = calculate_stats(10000, 10000, LOWER, RENT_LOWER, fx_rate=1.0)
assert m["savings_pct_delta"] > 0
assert m["savings_home_diff"] > 0
def test_savings_home_diff_consistent_with_pct():
m = calculate_stats(10000, 11000, HIGHER, RENT_HIGHER, fx_rate=1.0)
expected_pct = m["savings_home_diff"] / abs(m["savings_home"]) * 100
assert m["savings_pct_delta"] == pytest.approx(expected_pct)
@pytest.mark.parametrize("fx", [1.0, 0.30, 3.3])
@pytest.mark.parametrize("target_pct", [0, 10, 20, -15])
def test_savings_target_roundtrip(fx, target_pct):
"""required_net_new_for_savings_increase must produce a salary that,
when fed back into calculate_stats, yields the requested savings delta."""
net_home = 16000
need = required_net_new_for_savings_increase(net_home, target_pct, HIGHER, RENT_HIGHER, fx_rate=fx)
m = calculate_stats(net_home, need, HIGHER, RENT_HIGHER, fx_rate=fx)
assert m["savings_pct_delta"] == pytest.approx(target_pct, abs=1e-6)
def test_custom_weights_change_buckets():
# Savings 40%, of the remaining 60% rent takes half β†’ w_rent=0.3, w_non=0.3.
m = calculate_stats(10000, 10000, HIGHER, RENT_HIGHER, fx_rate=1.0, w_rent=0.3, w_non_rent=0.3)
assert m["rent_home"] == pytest.approx(3000)
assert m["non_rent_home"] == pytest.approx(3000)
assert m["savings_home"] == pytest.approx(4000) # 1 - 0.3 - 0.3
def test_savings_target_respects_custom_weights():
# Round-trip must still hold with non-default weights.
need = required_net_new_for_savings_increase(
12000, 15, HIGHER, RENT_HIGHER, fx_rate=0.30, w_rent=0.25, w_non_rent=0.45
)
m = calculate_stats(12000, need, HIGHER, RENT_HIGHER, fx_rate=0.30, w_rent=0.25, w_non_rent=0.45)
assert m["savings_pct_delta"] == pytest.approx(15, abs=1e-6)
def test_zero_target_matches_breakeven():
# 0% savings increase should equal the model's break-even salary.
fx = 0.30
need = required_net_new_for_savings_increase(16000, 0, HIGHER, RENT_HIGHER, fx_rate=fx)
m = calculate_stats(16000, need, HIGHER, RENT_HIGHER, fx_rate=fx)
assert need == pytest.approx(m["equiv_net_new_for_same_savings"])
def test_savings_pct_delta_none_when_home_savings_zero():
# weights sum to 1 β†’ savings_home = 0 β†’ percentage delta is undefined (None)
out = calculate_stats(5000, 5000, HIGHER, RENT_HIGHER, w_rent=0.5, w_non_rent=0.5)
assert out["savings_pct_delta"] is None