Untested Hotspots: The Highest-Leverage Coverage Targets
Untested hotspots are the files most likely to break your product and the least likely to catch it in CI. The signal is simple: high churn, low or zero coverage, and real production reach. A file with 50% coverage is not automatically dangerous. A file that changed eight times this month, sits on a hot path, and has no tests is. That is the test coverage priority list you actually want.
Coverage tools tell you what executed. coverage.py can also report branch coverage, which catches paths that statement coverage misses, and pytest-cov wraps that into a pytest workflow many Python teams already use. But coverage alone does not tell you what to test first. You need coverage gap detection plus change history plus dependency context. That is where untested hotspots earn their name. (coverage.readthedocs.io)
Coverage % is a leading question with a bad answer
A single percentage makes one promise and keeps another secret.
It tells you how much code ran. It does not tell you whether the right code ran. A repo at 82% can still hide a pile of untested hotspots in the files that route money, auth, billing, or data migrations. SonarQube’s docs treat coverage as one signal in a broader analysis flow, and it explicitly supports importing coverage reports from external tools rather than computing them itself. That matches the reality of most teams: coverage is an input, not a verdict. (docs.sonarsource.com)
A better question is:
- Which files change often?
- Which files sit on many dependency paths?
- Which files have weak or zero coverage?
- Which files have recent defect risk?
If you answer those four, you have a useful test coverage priority list.
Coverage Priority Map
The untested-hotspot pattern
An untested hotspot is a file with high change frequency and low test protection.
The pattern is common in codebases that move fast:
- a handler starts as a thin wrapper
- the wrapper gains branching logic
- more callers depend on it
- tests stay where the file used to be, not where it is now
That is how high churn untested files appear. The file was safe last quarter. It is not safe now.
Churn × coverage = risk
Hotspot analysis works because churn and coverage pull in opposite directions.
- Churn tells you where humans keep editing.
- Coverage tells you where tests keep running.
- Dependents tell you how wide the blast radius is.
A low-risk file can be untested if it rarely changes and has few callers. A high-risk file is the opposite: it changes often, sits near the center of the dependency graph, and has no tests or only shallow tests. Repowise’s docs describe hotspot scores from git history, ownership, co-change pairs, and bus factor. That combination is useful because it turns “this feels risky” into a ranked list of files and modules. (docs.repowise.dev)
A simple scoring model looks like this:
| Factor | Good sign | Bad sign |
|---|---|---|
| Churn | Low | High |
| Coverage | High | Zero or partial |
| Dependents | Few | Many |
| Branching | Flat | Many branches |
| Ownership | Shared | Single maintainer |
| Co-change history | Stable | Frequently touched with other risky files |
The strongest test coverage priority is usually the file that is bad on three or more of these rows.
Why 50% is a useful threshold
Fifty percent is not magic. It is a triage line.
Below 50%, you should assume the file is only partially exercised. In a branch-covered language, even “covered” lines can hide missed branches. Coverage.py’s branch mode is a good example: a line can be executed and still miss one of its outgoing paths. That matters more than raw line percentage when the code contains conditionals, error handling, or fallback logic. (coverage.readthedocs.io)
I use 50% as a practical cutoff for first-pass review because it catches three classes of file:
- Almost untested files — obvious gap.
- Legacy files with a few shallow tests — false comfort.
- Files with branch-heavy logic — line coverage hides misses.
If a file is above 50% and still shows up as a hotspot, that is a second-level review item. If it is below 50% and hot, it goes to the front of the line.
How to find them
You can find untested hotspots with a basic toolchain or with repo intelligence. The math is the same either way.
With repowise health/coverage
Repowise exposes hotspot analysis, ownership, dependency context, dead code, and the newer code-health layer with per-file health scores and refactoring targets ranked by impact per effort. That matters because untested hotspots are rarely isolated. They usually sit near other symptoms: declining health, hidden coupling, or repeated co-change. Repowise also ships MCP support, and the MCP spec defines a standard way for tools to expose structured context to clients through JSON-RPC-based messages and server features. (docs.repowise.dev)
A practical workflow looks like this:
- Open the hotspot view.
- Filter for low coverage or no coverage.
- Sort by churn.
- Check dependents.
- Inspect recent commit messages and co-change partners.
- Rank by blast radius, not by file size.
If you want to see the outputs on a real repo, start with our live examples. For a concrete graph view, the FastAPI dependency graph demo shows how file relationships surface the code that matters first. If you want to understand the internals, the architecture page explains how the layers fit together.
Hotspot Ranking Screen
With grep + git log + lcov
You do not need a platform to start. You need a script and discipline.
Use this when you want a quick pass on a small repo:
# 1) Find files with coverage output
lcov --list coverage.info
# 2) Inspect recent churn for a candidate file
git log --oneline --follow -- path/to/file.py
# 3) Count edits in a window
git log --since="90 days ago" --name-only --pretty=format: |
grep '^path/to/file.py$' | wc -l
# 4) Check the code for branches and risky patterns
grep -nE 'if |elif |try:|except |match ' path/to/file.py
Then combine the results manually:
- No hits in lcov: likely a coverage gap.
- Many commits in git log: high churn.
- Many branches: more paths to miss.
- Many callers: higher blast radius.
If you use Python, coverage.py can emit line, branch, HTML, XML, and JSON reports. That makes it easy to feed the results into a small ranking script or a CI job. SonarQube can then import those reports into its broader analysis pass if you already use it in your pipeline. (coverage.readthedocs.io)
What to do when you find them
Do not start by writing broad integration tests.
Untested hotspots need the narrowest test that proves the risky behavior. That keeps the test cheap and makes the failure mode obvious.
A good order of attack
-
Cover the branch, not the file.
If the hotspot has one risky conditional, write the test for both paths. -
Pin the contract at the boundary.
Test inputs and outputs at the file’s edge. Mock the rest only if it keeps the test stable. -
Add one regression test per bug.
If the file already broke once, freeze the bug in a test first. -
Write tests for co-change partners together.
If two files keep changing in the same commit, they probably encode one behavior split across two places. -
Refactor after the test lands.
A test without a refactor can still leave the hotspot in place. The point is to make future edits safer.
What not to do
- Do not chase overall coverage percentage.
- Do not add a large test that touches every dependency.
- Do not exclude the file just because it is ugly.
- Do not accept “it is hard to test” as a final answer.
If a file is genuinely dead, remove it. If it is alive, test the parts that keep changing.
Repowise’s hotspot analysis demo is a good reference for this workflow. The value is not the score alone. It is the combination of churn, ownership, dependents, and code health in one place. If you want to see the full shape of the repo before you touch a file, the auto-generated docs for FastAPI show what a file-level context layer looks like in practice.
Weekly Coverage Review Board
A weekly coverage review ritual
Treat coverage review like incident review. Same cadence. Same ownership. Same output.
Here is a ritual that works:
Monday: pull the list
- export files with zero or low coverage
- sort by churn over the last 30, 60, and 90 days
- mark files that moved into the top quartile
Tuesday: validate the blast radius
- list dependents
- check ownership
- inspect co-change partners
- flag files on auth, billing, persistence, or routing paths
Wednesday: pick one file
- write the smallest test that fails before the fix
- keep the test at the file boundary
- avoid over-mocking unless the dependency is flaky
Thursday: re-run coverage
- confirm the branch or line now reports as covered
- verify the new test does not create brittle setup
- compare the file’s risk score before and after
Friday: close the loop
- note what changed
- note what still needs work
- carry the next file into next week
That rhythm is more useful than a quarterly coverage push. It keeps the test coverage priority list current, and it stops the team from arguing about percentages that no longer map to risk.
If you are wiring this into an agent workflow, repowise exposes MCP tools for overview, context, risk, why, search, dependency paths, dead code, and architecture diagrams. The MCP spec’s model is a clean fit for this kind of structured repo context because clients ask for exactly the data they need instead of scraping text blobs. That is the right shape for a weekly review loop. (modelcontextprotocol.io)
Untested hotspots in CI
CI should not just fail on low global coverage. It should surface the files that deserve attention.
A good CI check can:
- compare the current coverage report with the previous main branch report
- flag new files with zero tests
- flag files whose churn rose sharply
- flag files that gained dependents but lost coverage
- post a short, deterministic summary on the pull request
Repowise’s AGPL-3.0 licensing matters here too. The license is designed for network server software and lets teams self-host the intelligence layer without depending on a closed service. The GNU project’s AGPL text is the canonical reference here. (gnu.org)
That makes the coverage workflow easier to keep inside your own CI and repo boundaries.
FAQ
What are untested hotspots?
Untested hotspots are files or modules with high change frequency, low or zero test coverage, and meaningful downstream impact. They are the first files I look at when deciding what to test first.
Is test coverage enough to find risk?
No. Coverage tells you what ran, not what is risky. You also need churn, dependents, ownership, and branch coverage. Coverage.py’s branch mode is especially useful because it catches missed paths inside covered lines. (coverage.readthedocs.io)
What should I test first in a large legacy repo?
Start with high churn untested files that sit on important paths: auth, billing, persistence, routing, migrations, and shared helpers. If a file is both hot and low coverage, it beats a larger but stable file every time.
How do I detect coverage gaps without a platform?
Use coverage reports, git log, and a dependency scan. In Python, coverage.py and pytest-cov give you line and branch reports. Pair that with git log --follow and a simple grep pass to identify hot code with weak test protection. (coverage.readthedocs.io)
Why does 50% coverage matter?
It is a practical triage threshold, not a law. Below 50%, the file is usually missing too much behavior to trust. Above 50%, you still need to check branch coverage and blast radius before you relax.
Can repowise help with coverage gap detection?
Yes. Repowise combines git intelligence, dependency graphs, and code health so you can rank untested hotspots by real risk instead of raw coverage alone. Start with repowise's architecture and then inspect the live examples to see how the pieces fit together.


