gendesign/backend/tests/test_poi_score.py
lekss361 b013fd886c
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m35s
Deploy / build-worker (push) Has been cancelled
feat(sf-b6): GET /parcels/{cad}/poi-score — weighted top-7 (#333)
2026-05-17 21:11:25 +00:00

168 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for POI weighted score service (B6).
Юнит-тесты для чистой функции — без DB.
"""
from app.services.site_finder.poi_score import (
CATEGORY_WEIGHTS,
PoiScoreResponse,
_category_weight,
compute_poi_weighted_top7,
)
# ── unit: _category_weight ─────────────────────────────────────────────────────
def test_category_weight_metro():
"""Метро имеет наибольший вес из всех категорий."""
metro_w = _category_weight("metro_stop")
for cat in CATEGORY_WEIGHTS:
if cat != "metro_stop" and cat != "default":
assert metro_w >= _category_weight(
cat
), f"metro_stop weight {metro_w} должен быть >= {cat} weight {_category_weight(cat)}"
def test_category_weight_unknown_returns_default():
w = _category_weight("unknown_category_xyz")
assert w == CATEGORY_WEIGHTS["default"]
def test_category_weight_all_positive():
"""Все веса в CATEGORY_WEIGHTS должны быть положительными (B6 — ranking, не штраф)."""
for cat, w in CATEGORY_WEIGHTS.items():
assert w > 0, f"Вес {cat}={w} должен быть > 0"
# ── unit: weight formula ratio ─────────────────────────────────────────────────
def test_weight_formula_ratio():
"""Ближний объект той же категории должен иметь больший вес."""
cat = "school"
cw = _category_weight(cat)
w_near = (1.0 / (100.0 + 100.0)) * cw # 100м
w_far = (1.0 / (1000.0 + 100.0)) * cw # 1000м
assert w_near > w_far
def test_weight_formula_category_dominates_at_equal_distance():
"""При одинаковом расстоянии метро должно быть впереди автобусной остановки."""
dist = 500.0
w_metro = (1.0 / (dist + 100.0)) * _category_weight("metro_stop")
w_bus = (1.0 / (dist + 100.0)) * _category_weight("bus_stop")
assert w_metro > w_bus
# ── unit: compute_poi_weighted_top7 with mock DB ───────────────────────────────
class _MockMappings:
def __init__(self, data: list[dict]) -> None:
self._data = data
def all(self) -> list[dict]:
return self._data # type: ignore[return-value]
class _MockResult:
def __init__(self, data: list[dict]) -> None:
self._data = data
def mappings(self) -> "_MockMappings":
return _MockMappings(self._data)
class _MockDb:
"""Минимальный мок SQLAlchemy Session для тестирования без БД."""
def __init__(self, rows: list[dict]) -> None:
self._rows = rows
def execute(self, *_args: object, **_kwargs: object) -> _MockResult:
return _MockResult(self._rows)
def _make_row(name: str, category: str, distance_m: float) -> dict:
return {
"name": name,
"category": category,
"tags": {},
"distance_m": distance_m,
}
def test_top7_returns_at_most_7():
rows = [_make_row(f"POI {i}", "school", float(i * 50)) for i in range(1, 20)]
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
assert isinstance(result, PoiScoreResponse)
assert len(result.top_poi) <= 7
def test_top7_sorted_by_weight_desc():
rows = [
_make_row("Дальняя школа", "school", 1500.0),
_make_row("Метро", "metro_stop", 300.0),
_make_row("Близкая школа", "school", 100.0),
]
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
weights = [item.weight for item in result.top_poi]
assert weights == sorted(weights, reverse=True), "top_poi должны быть по weight DESC"
def test_metro_beats_school_at_equal_distance():
"""Метро в 300м должно быть на первом месте перед школой в 300м (равное расстояние)."""
rows = [
_make_row("Школа №1", "school", 300.0),
_make_row("Метро Чкаловская", "metro_stop", 300.0),
]
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
assert (
result.top_poi[0].category == "metro_stop"
), "При равном расстоянии метро (category_weight=6.0) должно быть выше школы (5.0)"
def test_metro_first_when_close():
"""Метро в 50м должно быть на первом месте перед школой в 300м."""
rows = [
_make_row("Школа №1", "school", 300.0),
_make_row("Метро Чкаловская", "metro_stop", 50.0),
]
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
assert result.top_poi[0].category == "metro_stop", (
"Метро (weight=6.0) в 50м должно быть впереди школы (weight=5.0) в 300м — "
f"metro_weight={(1/(50+100))*6:.5f} vs school_weight={(1/(300+100))*5:.5f}"
)
def test_empty_db_returns_empty_top_poi():
db = _MockDb([])
result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
assert result.top_poi == []
assert result.cad_num == "66:41:0204016:10"
assert result.radius_m == 2000
def test_address_built_from_tags():
rows = [
{
"name": "Магазин",
"category": "shop_small",
"tags": {"addr:street": "ул. Ленина", "addr:housenumber": "10"},
"distance_m": 200.0,
}
]
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "test", 56.838, 60.605)
assert result.top_poi[0].address == "ул. Ленина, 10"
def test_address_none_when_no_tags():
rows = [_make_row("Парк", "park", 400.0)]
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "test", 56.838, 60.605)
assert result.top_poi[0].address is None