ScreenshotNeo

BlogGuides

How to Find Blind Spots in Your Test Coverage

Coverage shows which code ran, not whether tests would catch a bug. Use this workflow to find untested paths, weak assertions, and missing user journeys.

By the ScreenshotNeo team4 October 20269 min read

To find blind spots in test coverage, treat the coverage report as a map of code that did and did not run—not as a score for test quality. Measure the intended source files, inspect uncovered lines and branches, compare the gaps with requirements and user journeys, then probe important logic with mutation testing. Add focused tests for the highest-risk gaps and review the report again.

A line can execute without its behavior being asserted. A test suite can also miss a user-facing journey even when many individual lines run. Investigate coverage at these separate levels: code execution, control-flow outcomes, assertion strength, requirements, and UI journeys.

1. Establish a trustworthy baseline

Start by checking that the coverage tool measures the application code you intend to test. A report that includes only files already imported during the run can hide entire modules that no test reached. In Coverage.py, --source=. scopes measurement to the current directory and helps report files that were not executed. Exclude generated or irrelevant files deliberately, and make exclusions visible to the team.

Here is a runnable Python example using pytest and Coverage.py. It creates a tiny application with two branches, then runs its tests with line and branch coverage.

mkdir coverage-blind-spots
cd coverage-blind-spots
python -m venv .venv
# Activate the environment for your shell:
# macOS/Linux: source .venv/bin/activate
# Windows PowerShell: .venv\\Scripts\\Activate.ps1
python -m pip install pytest coverage
mkdir -p src tests
cat > src/pricing.py <<'PY'
def total(price, discount):
    if price < 0:
        raise ValueError("price must be non-negative")
    if discount < 0 or discount > 1:
        raise ValueError("discount must be between 0 and 1")
    return price * (1 - discount)
PY
cat > tests/test_pricing.py <<'PY'
import pytest
from src.pricing import total

def test_full_price():
    assert total(100, 0) == 100

def test_discounted_price():
    assert total(100, 0.2) == 80

def test_negative_price_is_rejected():
    with pytest.raises(ValueError, match="price"):
        total(-1, 0)
PY
python -m coverage run --branch --source=src -m pytest
python -m coverage report -m
python -m coverage html
# Open htmlcov/index.html in a browser to inspect annotated source.

The example intentionally does not test discount values below zero or above one. The report can point you toward those missing paths. The exact setup for other languages depends on the test runner and coverage tool: ensure your runner emits coverage data, scope it to production code, and enable its branch or condition metric when available. VS Code can show coverage in its Test Coverage view, editor gutter, Explorer, and diff editor when the installed testing extension supports coverage. VS Code testing and coverage documentation.

2. Read the report at file, line, and branch level

Do not stop at the aggregate percentage. Review the file list, look for files at zero coverage, then inspect uncovered lines and partial branches. Prioritize code that controls business rules, permissions, billing, data changes, error handling, and externally visible behavior.

Coverage view What it tells you Question to ask
File Whether a module appears to have been exercised Is this production module absent from every test run?
Line or statement Whether statements executed Which error handling, fallback, or boundary lines never ran?
Branch Which outcomes of control-flow decisions ran Did tests take both the true and false outcomes?
Condition or MC/DC Whether Boolean conditions independently affect a decision, where supported Are combinations of safety-critical conditions adequately exercised?

Line coverage can hide a decision gap. For example, a test that calls if is_admin: allow() only with an administrator executes the decision line but says nothing about the denial path. Coverage.py supports branch measurement; its report identifies missing branch destinations. Coverage.py 7.14.3 documentation.

For decision-heavy or safety-critical systems, check whether decision, condition, MC/DC, or relational boundary criteria apply and whether the tool supports them. These criteria add useful detail for certain domains; they are not mandatory for every ordinary web application. MathWorks documents these metrics for model and code verification in Simulink Coverage.

3. Compare code gaps with requirements and user flows

A coverage report describes execution, not whether the product’s important behavior has a test. Compare it with requirements, acceptance criteria, business rules, and real user paths. For each important behavior, identify the normal case, meaningful boundary values, invalid input, permissions or roles, and failure or recovery state.

  • Which requirements have no test linked to them?
  • Which user roles, states, or transitions are absent from the suite?
  • What should happen when a dependency times out, returns bad data, or rejects a request?
  • Are empty, minimum, maximum, duplicate, stale, and malformed values covered where they matter?
  • Can a user complete the key journey, including validation errors and recovery?

For browser applications, source coverage and UI journey coverage answer different questions. Cypress UI Coverage uses Test Replay DOM snapshots captured in Cypress Cloud to report untested interactive elements and linked pages. It can help identify UI controls absent from recorded runs; it complements source-code metrics rather than replacing them. See Cypress UI Coverage documentation.

4. Check whether tests would detect a defect

Execution is not proof that an assertion would fail when behavior is wrong. A test may call a function but assert only that it did not throw; it may assert an implementation detail while missing the user-visible result. Inspect tests around high-impact covered code and ask: “What specific incorrect change would make this test fail?”

Mutation testing helps answer that question. A mutation tool makes small deliberate code changes—such as changing <= to <—and reruns tests. A killed mutant caused tests to fail; a surviving mutant passed; a timeout could not finish within its limit. A survivor is a lead to investigate, not automatic proof a new test is required: the mutation can be equivalent, irrelevant, or noisy. Microsoft Learn describes this workflow with Stryker.NET, which applies to .NET projects: Mutation testing in .NET.

For a .NET project, the documented basic flow is to install Stryker.NET and run it from the unit-test project directory:

dotnet tool install -g dotnet-stryker
cd path/to/your/test-project
dotnet stryker

Review the HTML report and investigate surviving mutants in high-risk behavior first. Mutation runs add work because the tool reruns tests against changed code; use them selectively on critical modules or changed areas. Do not adopt a universal mutation-score target without considering the code, the mutations, and the cost of meaningful tests.

5. Prioritize gaps by risk, then add focused tests

Rank a gap by potential user or business impact, requirement importance, likelihood of the condition occurring, and the effort required to add a useful test. A missed branch in a payment authorization rule deserves more attention than an untested formatting helper. A justified exclusion may be appropriate for generated code or unreachable defensive paths, but document the reason and revisit it when the code changes.

  1. Pick one high-impact uncovered line, branch, requirement, or UI path.
  2. State the expected behavior and the defect the test should catch.
  3. Add the smallest test that exercises the relevant input or state and asserts the observable result.
  4. Run that test, then the relevant suite with coverage.
  5. For critical logic, run mutation checks and inspect survivors.
  6. Review whether the new report reflects a meaningful gap closed; record any deliberate exclusion.

6. Troubleshoot misleading or missing results

Symptom Likely cause What to check or fix
Coverage is unexpectedly high, but whole modules are untested The measured source scope omits files that were never imported. Configure source scope to include the intended application code; with Coverage.py, use --source=. or the relevant package path.
No coverage data appears The test runner did not collect or export coverage, or the editor extension does not support the runner’s format. Run the coverage command directly, confirm it creates a report or data file, and check the testing extension’s coverage support.
Tests run, but branch coverage remains incomplete Only one outcome ran, or branch measurement is disabled or unsupported in the current setup. Enable branch coverage in the runner and add tests for meaningful alternate outcomes.
Coverage includes tests, dependencies, or generated files Scope and exclusion rules are too broad. Include the application source explicitly and exclude irrelevant files with documented rules.
Coverage differs between local and CI runs Different test selections, environment conditions, generated artifacts, or coverage settings were used. Compare commands, configuration, runtime versions, and test inputs; collect compatible runs before combining data.
A mutant survives even though the line is covered The assertion does not distinguish the original behavior from the mutation, or the mutant is equivalent/noisy. Inspect the behavioral contract and mutant; add a test only if it captures a meaningful missed behavior.
UI controls still appear untested The recorded browser runs may not reach that view or interact with that element. Check the captured journeys and add or repair the relevant UI test; confirm the coverage product has the needed run data.

7. Improve performance and reliability of the coverage workflow

  • Keep scope narrow and explicit. Measuring application code rather than every dependency makes reports easier to interpret and can reduce collection work.
  • Use the right cadence. Run fast line or branch coverage with normal test workflows; schedule broader mutation runs for selected modules or CI stages where their added runtime is useful.
  • Make reports reproducible. Keep runner configuration and exclusions in version control, and use consistent test selection and environment settings when comparing reports.
  • Interpret thresholds in context. A percentage is a signal for review, not a guarantee of correctness. There is no universal target established by the cited documentation for every project.
  • Watch for measurement limits. Coverage tools differ in supported metrics, language integrations, and treatment of generated or dynamic code. Confirm what the chosen tool actually measures before comparing scores.

JetBrains summarizes the caveat directly: “Code coverage doesn’t express the quality of tests or application logic but instead serves as a guidance that can be used in prioritizing application development and testing activities.” dotCover documentation, Basic Terms.

Or skip the browser setup

If a browser test or review workflow needs a page screenshot, ScreenshotNeo returns an image or PDF from one GET request. The same request can help capture a page while investigating a UI journey; it does not replace code coverage or prove that a test assertion is strong. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.

Frequently asked questions

Does 100% coverage mean my tests are good?

No. It means the measured code met the selected execution criterion. It does not establish that assertions detect incorrect behavior or that requirements and user journeys are covered.

Should I aim for a specific coverage percentage?

Use targets only in context: define what is measured, consider risk, and use the threshold to prompt investigation. The same percentage can conceal very different uncovered code and test strength.

Which gap should I fix first?

Start with untested behavior whose failure could harm users or violate a critical requirement, especially when a missing branch or weak assertion affects that behavior.

Are UI coverage and code coverage interchangeable?

No. Source coverage tracks execution of code; UI coverage can reveal controls or pages absent from recorded browser journeys. Use the view that answers the question you are investigating.