tepyd reach¶
Do unit-tier tests stay inside the unit under test?
A real unit test exercises one unit in isolation. reach checks that statically: for every source unit, each test file at the unit tier should import only that unit, plus its own sub-modules, a configurable shared allow-list, and any excluded units. An import that resolves to a different source unit is a leak: the test reaches across a package boundary, so it isn't really isolated, and a change in that other package can break this "unit" test.
Output¶
Reach — unit tests crossing into other units (5 leaks across 2 units)
modules/wire: 3 leaks
tests/a_unit/modules/wire/test_flow.py → modules/biz
tests/a_unit/modules/wire/test_x.py → models, services
checkout: 2 leaks
tests/a_unit/checkout/test_pay.py → payments
12/14 unit(s) isolated at the unit tier.
Suppressing the legitimate ones¶
Most leaks on a real codebase are imports of a shared kernel (domain models, settings, framework glue, test helpers) that every unit may legitimately use. List those units in shared (which matches the unit and its subtree) and their imports stop counting:
Given a run reporting:
services: 20 leak(s)
tests/a_unit/services/test_invoicing.py → models
tests/a_unit/services/test_menus.py → settings
tests/a_unit/services/stripe/test_price_model.py → flask, models
tests/a_unit/services/test_opengraph.py → models, modules/wire
that shared list drops everything except the modules/wire reach, a cross-feature import that no allow-list should cover. (If wire really is shared infrastructure, add it too.)
Three levers, in order:
shared. A unit every unit test may import. The common case above.exclude. Drop a whole unit from analysis, with a required reason: faker, generated code, a unit you test only via integration.- Move the test. If a unit test genuinely needs another feature, it isn't a unit test; relocate it to the integration tier.
There is no per-line # noqa. A leak is either shared (declare it once) or real (fix it, or move the test). Silencing individual lines would only hide the signal.
Why this is not an import linter¶
Tools like import-linter or tach enforce your production import graph globally. The reach lens enforces test isolation, keyed to the tests↔units mapping Tepyd already owns, and needs no per-unit configuration. It is a ranked heuristic, not a gate: it exits 0 even with leaks. A direct from app.payments import Money might just be a shared type living in the wrong package. You judge.
What it cannot see¶
Stated up front. reach inspects a test file's own imports, which is the right surface: unlike I/O purity (which hides in fixtures), a test that uses another package imports it directly. But:
- re-exports (
app.are-exporting absymbol) are invisible; - collaborators handed in by a
conftestfixture are invisible; - dynamic imports are invisible;
- relative imports and non-
test_*.pyfiles are not scanned; - imports inside a
TYPE_CHECKINGblock are ignored, as they should be.
So recall has a floor: a reported leak is real, while a clean result proves nothing about isolation.
--json¶
{
"tier": "unit",
"units_checked": 8,
"isolated": 2,
"leaks": [
{
"unit": "lenses/report",
"files": [
{
"file": "tests/a_unit/lenses/test_report.py",
"foreign": ["core/config", "core/model", "lenses/gaps"]
}
]
}
],
"skipped": []
}
tier is the unit tier's label, and skipped lists units that were excluded from the check.