DEV Community

Dakota Huang
Dakota Huang

Posted on

Your Refactor May Only Touch Lines Your Tests Execute

The rule is one sentence. Every line you add must appear in the coverage report. Not the file. Not the module. The line.

I adopted it after three refactors that passed review and still broke production. Each diff moved code that no test ever entered. Each diff looked small.

Line counts and dependency scores measure the shape of a change. They cannot tell you whether the suite has ever run the code you are touching. Coverage can.

The loop in four steps

Step 1: record coverage on the untouched tree. This is your baseline.

python -m coverage run --branch -m pytest -q
python -m coverage json -o coverage-before.json
Enter fullscreen mode Exit fullscreen mode

--branch is worth the extra time. It records which side of an if actually ran, not just which lines existed.

Step 2: make the smallest change you can defend. Commit it. Do not bundle a rename with a move.

Step 3: record coverage again, on the changed tree.

python -m coverage run --branch -m pytest -q
python -m coverage json -o coverage.json
Enter fullscreen mode Exit fullscreen mode

Step 4: diff against the base branch and let the gate compare both artifacts.

git diff -U0 origin/main...HEAD > change.diff
python tools/covered_edit_gate.py change.diff coverage.json
Enter fullscreen mode Exit fullscreen mode

-U0 matters. With zero context lines, every range in the diff is an added line. No guessing, no hunk arithmetic.

The gate itself

The script is short. It reads two files and answers one question per added line: did the suite execute this line?

# tools/covered_edit_gate.py
import json, re, sys

diff_path, cov_path = sys.argv[1], sys.argv[2]
SKIP_PREFIX = ("tests/", "migrations/", "vendor/")
HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")

def added_ranges(diff_text):
    file, out = None, {}
    for line in diff_text.splitlines():
        if line.startswith("+++ b/"):
            file = line[6:].strip()
            out.setdefault(file, set())
        elif line.startswith("@@") and file:
            m = HUNK.match(line)
            start, count = int(m.group(1)), int(m.group(2) or 1)
            out[file].update(range(start, start + count))
    return {f: ls for f, ls in out.items() if ls}

def executed_lines(cov_path):
    files = json.load(open(cov_path))["files"]
    return {p.replace("\\", "/"): set(i["executed_lines"]) for p, i in files.items()}

def is_code(path, lineno):
    try:
        text = open(path, encoding="utf-8").read().splitlines()[lineno - 1]
    except (OSError, IndexError):
        return True
    text = text.strip()
    return bool(text) and not text.startswith("#")

def main():
    diff = open(diff_path, encoding="utf-8").read()
    hit = executed_lines(cov_path)
    failures = []
    for path, lines in added_ranges(diff).items():
        if path.startswith(SKIP_PREFIX):
            continue
        code = {n for n in lines if is_code(path, n)}
        missing = sorted(code - hit.get(path, set()))
        if missing:
            failures.append((path, missing))
    for path, missing in failures:
        print(f"{path}: {len(missing)} added line(s) never executed")
        for n in missing[:10]:
            print(f"  line {n}")
    print("gate: PASS" if not failures else "gate: FAIL")
    sys.exit(1 if failures else 0)

main()
Enter fullscreen mode Exit fullscreen mode

A failing run looks like this. It names the file and the exact lines that no test reached.

$ python tools/covered_edit_gate.py change.diff coverage.json
src/rates.py: 4 added line(s) never executed
  line 88
  line 89
  line 94
  line 97
gate: FAIL
Enter fullscreen mode Exit fullscreen mode

What it refuses, and why that is correct

Change Gate result Reason
New line inside a covered function Pass The suite runs that code path
New module imported and called by tests Pass Post-change coverage lists its executed lines
New module nobody imports Fail You added dead code
New if branch never taken Fail Unreached branch, use --branch
Blank line, comment, docstring line Skipped Not a statement
Deleted lines Not judged Deletions need a different check

That last row is honest. The gate constrains additions. Deletions are covered by your suite failing, or by nobody noticing.

A shipping-cost calculator that failed the gate

Our shipping_quote() function had a 70-line body and two call sites. Tests covered the domestic path only. The international branch had zero executed lines for at least a year.

The first extraction attempt moved _zone_rate() out of that branch. The gate failed on four lines. That failure was correct. I had no evidence the branch worked before the move. Any behavior I preserved was imaginary.

So the smallest safe change became a test, not a refactor. I covered the international branch first, confirmed the new test failed when I inverted the branch condition, then ran the extraction again. Second attempt passed. Same three commits as before, one of them just tests.

Where free model access helps, and where it does not

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Covering an uncovered branch is the boring part. I used MonkeyCode's free model access to draft those missing-branch tests from the function signature and a few fixture rows. Roughly half the drafts were usable; the rest asserted the current output without exercising the branch.

Every drafted test went through one manual check. I inverted the branch under test and confirmed the assertion failed. A test that still passes on broken code is decoration.

The nightly run of the full suite is where I used the free server option. I kept the harness single-process with no external services, so it needs nothing but Python and a checkout. Local developers run the same two commands and get the same report.

Limitations you should accept up front

Coverage proves execution, not correctness. A line run once, inside a test with no assertion, passes this gate and proves nothing.

Async callbacks and registered handlers can be defined without ever running. The gate will flag them, and it should.

Generated code and vendored directories must go in SKIP_PREFIX. Otherwise the report fills with noise and people stop reading it.

Large suites make coverage expensive. Run it nightly, then gate pull requests against the cached report.

Who should not use this

Skip this if your repo has no runnable test suite. Build one first; the gate has nothing to read.

Skip it on hotfix branches where speed outranks evidence. Use it on the cleanup branches that keep breaking things.

Skip it in polyglot repos where coverage tooling only half works. A gate that silently passes is worse than no gate.

If you want to test the idea cheaply, run coverage json on your messiest module today. Count how many lines you were about to move are actually executed. That number will change your diff.

Top comments (0)