# tests/support/ — общие test-инструменты (не сами тесты) ## parity.py — legacy → scraper_kit parity harness (issue #2304) Инструмент для issues #2305-#2310 (миграция неймигрированных importers `app/services/scrapers/*` → `scraper_kit` эквиваленты, см. audit `Scraper_Kit_Legacy_Dependency_Audit_0703` в vault). Каждая такая миграция должна была доказать, что kit-путь даёт ТОТ ЖЕ результат, что и legacy-путь на одном и том же входе — для этого использовался `assert_parity`. Легаси `app/services/scrapers/` каталог полностью удалён (#2397 финальный шаг E, миграция завершена) — все golden-parity/legacy-vs-kit тесты, построенные на `assert_parity`, удалены вместе с ним (kit — единственный живой путь). Harness (`parity.py`) остаётся в дереве на случай будущих strangler-миграций (например legacy-кода за пределами `app/services/scrapers/`). ### Быстрый старт (иллюстративный пример; `legacy_fn` — гипотетическая функция ### из будущего strangler-миграции модуля, не из уже удалённого `app/services/scrapers/`) ```python from some_legacy_module import evaluate_via_cian as legacy_fn from scraper_kit.providers.cian.valuation import evaluate_via_cian as kit_fn from tests.support.parity import assert_parity def test_cian_valuation_parity() -> None: assert_parity( legacy_fn=legacy_fn, kit_fn=kit_fn, fixtures=[ (fixture_html_1, "https://cian.ru/flat/1"), (fixture_html_2, "https://cian.ru/flat/2"), ], ignore_fields={"latency_ms", "fetched_at"}, # недетерминированные поля tolerance=1e-6, # допуск для float-полей (напр. рассчитанные оценки) ) ``` ### Как формировать `fixtures` Каждый элемент списка — один тестовый вход: - `tuple`/`list` → распаковывается как позиционные аргументы: `legacy_fn(*fixture)`; - любое другое значение (str, dict, ...) → передаётся как единственный позиционный аргумент: `legacy_fn(fixture)`. Начните с 1-2 hardcoded HTML-фикстур/dict'ов (git-история `tests/scrapers/test_avito_detail_kit_parity.py`, удалён вместе с legacy `app/services/scrapers/` в #2397, содержит референсный пример). DB-фикстуры НЕ нужны для чистых parse/compute-функций — используйте их только если сама legacy/kit-функция реально требует Session. ### Почему нельзя просто `==` legacy- и kit-версии одного и того же dataclass (напр. `DetailEnrichment`, `CianValuationResult`) — это РАЗНЫЕ Python-классы (живут в разных модулях), даже если поля идентичны. Дефолтный `dataclass.__eq__` сначала проверяет `other.__class__ is self.__class__` — для двух разных классов это всегда `False`, ДАЖЕ когда все значения полей совпадают. `assert_parity` / `compare_outputs` нормализуют оба вывода в dict/list/scalar (через `dataclasses.fields()` / `.model_dump()` рекурсивно) и сравнивают СТРУКТУРНО, по именам полей — эта проблема класс-идентичности не мешает. ### ignore_fields vs tolerance - `ignore_fields={"latency_ms", "fetched_at", ...}` — поле целиком исключается из сравнения на ЛЮБОМ уровне вложенности. Используйте для полей, у которых даже приблизительное совпадение не гарантировано (timestamps, request-id). - `tolerance=1e-6` — числовой (`int`/`float`, НЕ `bool`) допуск через `math.isclose(rel_tol=tolerance, abs_tol=tolerance)`. Используйте для float-полей, где legacy/kit могут давать чуть разное значение из-за порядка операций с плавающей точкой (не для timestamps/datetime — там используйте `ignore_fields`). ### При мисматче `assert_parity` кидает `ParityMismatchError` (подкласс `AssertionError`) со списком ВСЕХ различающихся полей: путь до поля + значение legacy + значение kit. Не просто "not equal" — сразу видно, что чинить. ### Не входит в scope harness'а - Он НЕ загружает DB-фикстуры сам — если legacy/kit функция требует `Session`, передавайте mock/session в fixture-tuple как обычно. Live network/DB в parity-тестах избегайте — они должны быть detereministic offline unit-тестами. - Он НЕ мигрирует сами importers — это делает каждый sub-issue #2305-#2310 отдельно (тесты для конкретной пары legacy/kit функций пишет тот sub-issue).