Skip to content

Anti mirror

anti_mirror

Anti-mirror rule — integration/e2e tests must not be named after source modules.

Industry convention (pyOpenSci, Real Python, the Pyramid testing literature): tests/integration/ and tests/e2e/ test files describe a scenario or a user action, not a source module. A tests/integration/test_foo.py whose basename collides with src/<pkg>/.../foo.py is almost always a misplaced unit test — it duplicates the unit-mirror surface (MirrorRule) and obscures the behaviour the integration suite is supposed to verify.

This rule walks tests/integration/**/test_*.py and tests/e2e/**/test_*.py and flags every test file whose test_<name>.py shadows a source module basename. tests/unit/ is never walked here — that's MirrorRule's job.

AntiMirrorRule dataclass

Bases: ProjectRule

Flag integration/e2e tests named after source modules.

Integration and e2e tests are scenario-named by convention. A tests/integration/test_foo.py that mirrors src/<pkg>/foo.py is almost always a misplaced unit test — promote it to tests/unit/ or rename to describe the verified scenario.

Sources matched by [tool.axm-audit.mirror].exempt_paths are excluded: a CLI-wrapper module like commands/data.py exempted by AXM-1666 is legitimately covered by tests/integration/test_data.py.

Scoring: max(0, 100 - len(violations) * 15); passed = score >= 90.

Source code in packages/axm-audit/src/axm_audit/core/rules/practices/anti_mirror.py
Python
@dataclass
@register_rule("practices")
class AntiMirrorRule(ProjectRule):
    """Flag integration/e2e tests named after source modules.

    Integration and e2e tests are scenario-named by convention. A
    ``tests/integration/test_foo.py`` that mirrors ``src/<pkg>/foo.py`` is
    almost always a misplaced unit test — promote it to ``tests/unit/`` or
    rename to describe the verified scenario.

    Sources matched by ``[tool.axm-audit.mirror].exempt_paths`` are excluded:
    a CLI-wrapper module like ``commands/data.py`` exempted by AXM-1666 is
    legitimately covered by ``tests/integration/test_data.py``.

    Scoring: ``max(0, 100 - len(violations) * 15)``; ``passed = score >= 90``.
    """

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule."""
        return "PRACTICE_TEST_SCENARIO_NAMING"

    def check(self, project_path: Path) -> CheckResult:
        """Walk integration/e2e and flag tests named after source modules."""
        early = self.check_src(project_path)
        if early is not None:
            return early

        tests_path = project_path / "tests"
        if (
            not (tests_path / "integration").is_dir()
            and not (tests_path / "e2e").is_dir()
        ):
            return CheckResult(
                rule_id=self.rule_id,
                passed=True,
                message="No integration or e2e tests to check",
                severity=Severity.INFO,
                score=100,
                details={"anti_mirror": []},
            )

        config = _load_mirror_config(project_path)
        if config.error is not None:
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message="Invalid mirror config",
                severity=Severity.WARNING,
                score=0,
                details={"anti_mirror": []},
                fix_hint=config.error,
            )

        src_path = project_path / "src"
        source_basenames = _collect_source_basenames(src_path, config.exempt_paths)
        violations = _collect_anti_mirror_violations(tests_path, source_basenames)

        if not violations:
            return CheckResult(
                rule_id=self.rule_id,
                passed=True,
                message="No anti-mirror naming violations",
                severity=Severity.INFO,
                score=100,
                details={"anti_mirror": []},
            )

        score = max(0, 100 - len(violations) * 15)
        passed = score >= 90  # noqa: PLR2004

        shown = violations[:5]
        tail = (
            f" (+{len(violations) - 5} more)" if len(violations) > 5 else ""  # noqa: PLR2004
        )
        text = "• anti-mirror: " + " ".join(shown) + tail

        first = violations[0]
        fix_hint = (
            f"{first} → rename to test_<scenario>.py describing what the test "
            "verifies (scenario-named, not source-mirrored)"
        )

        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=(
                f"{len(violations)} integration/e2e test(s) named after source modules"
            ),
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=int(score),
            details={"anti_mirror": violations},
            fix_hint=fix_hint,
            text=text,
        )
rule_id property

Unique identifier for this rule.

check(project_path)

Walk integration/e2e and flag tests named after source modules.

Source code in packages/axm-audit/src/axm_audit/core/rules/practices/anti_mirror.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Walk integration/e2e and flag tests named after source modules."""
    early = self.check_src(project_path)
    if early is not None:
        return early

    tests_path = project_path / "tests"
    if (
        not (tests_path / "integration").is_dir()
        and not (tests_path / "e2e").is_dir()
    ):
        return CheckResult(
            rule_id=self.rule_id,
            passed=True,
            message="No integration or e2e tests to check",
            severity=Severity.INFO,
            score=100,
            details={"anti_mirror": []},
        )

    config = _load_mirror_config(project_path)
    if config.error is not None:
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message="Invalid mirror config",
            severity=Severity.WARNING,
            score=0,
            details={"anti_mirror": []},
            fix_hint=config.error,
        )

    src_path = project_path / "src"
    source_basenames = _collect_source_basenames(src_path, config.exempt_paths)
    violations = _collect_anti_mirror_violations(tests_path, source_basenames)

    if not violations:
        return CheckResult(
            rule_id=self.rule_id,
            passed=True,
            message="No anti-mirror naming violations",
            severity=Severity.INFO,
            score=100,
            details={"anti_mirror": []},
        )

    score = max(0, 100 - len(violations) * 15)
    passed = score >= 90  # noqa: PLR2004

    shown = violations[:5]
    tail = (
        f" (+{len(violations) - 5} more)" if len(violations) > 5 else ""  # noqa: PLR2004
    )
    text = "• anti-mirror: " + " ".join(shown) + tail

    first = violations[0]
    fix_hint = (
        f"{first} → rename to test_<scenario>.py describing what the test "
        "verifies (scenario-named, not source-mirrored)"
    )

    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=(
            f"{len(violations)} integration/e2e test(s) named after source modules"
        ),
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=int(score),
        details={"anti_mirror": violations},
        fix_hint=fix_hint,
        text=text,
    )