September 28, 2026

Dead Tests in CI/CD in 2026: How to Find, Diagnose, and Remove Tests That Never Run, Always Pass, and Hide Real Failures

CI/CD pipelines routinely report hundreds of passing tests while running a portion of them in states that make the results meaningless. Tests that were disabled during a debugging session, tests that depend on skipped conditional blocks, tests that have had their assertions removed, and tests that never execute their intended code paths all contribute to a false passing count. Finding and removing dead tests in 2026 is a matter of reading execution logs and coverage data with the intent to disprove stability rather than confirm it.
This guide covers the three categories of dead tests, how to detect each type using the tools available in modern CI/CD pipelines, how to diagnose root causes before deletion, and how to remove tests safely without creating actual coverage regressions. Teams looking for structural context can review Astaqc’s complete software testing guide and test automation services.
The term dead test describes any test that is nominally part of a suite but does not provide reliable signal about the application’s correctness. Three distinct failure modes produce dead tests, and each requires different detection and remediation approaches.
Tests that never run are the most straightforward. These are tests marked with skip, xfail, or pending annotations, tests inside conditionals that never evaluate to true in the CI environment, or tests excluded by a configuration filter that was set during an emergency and never revisited. CI pipelines report these as skipped, but the report hides whether the skip was intentional and time-bounded or indefinite.
Tests that always pass are harder to identify because the pipeline reports them as green. A test always passes when its assertions are evaluating trivial conditions — checking that an HTTP response is not null rather than that it has the expected status code, or confirming that a page contains the word “success” rather than that the specific transaction was recorded. These tests run, record results, and contribute to coverage metrics without providing any protection against regressions in their intended code paths.
Tests that hide real failures represent the most dangerous category. These are tests that use retry-until-pass logic without an assertion on the eventual state, tests that catch exceptions and mark themselves as passed regardless of the exception type, and tests that compare against a baseline that was last updated when the application was in a broken state. When a real regression occurs, these tests absorb the failure signal rather than surfacing it.
Detection requires combining three data sources: execution logs, code coverage reports, and test result history over time. No single source is sufficient because each category of dead test is invisible to a different one.
Execution logs reveal tests that never run. In GitHub Actions and most CI systems, job logs include a summary of skipped tests with reasons. The useful signal is in the reason field: a skip annotation citing an issue number from 14 months ago is a dead test; a skip from last week with an open issue is a temporary quarantine. Build tooling that does not record skip reasons makes this distinction invisible, which is itself a process gap worth addressing before running a dead-test audit.
Code coverage reports surface tests that run but do not execute their target code. A test marked as passing that contributes zero additional line coverage to its module is executing, but something in its path — a mock boundary, a test data fixture, or a conditional guard — is preventing it from reaching the code it was written to cover. Istanbul, Coverage.py, JaCoCo, and similar tools identify the uncovered lines; mapping those lines back to test files identifies which tests should be covering them but are not.
| Dead Test Type | Detection Signal | Primary Tool |
|---|---|---|
| Never runs | Skip count in test summary | CI job logs, pytest -v, jest --verbose |
| Always passes | Zero failure rate over 90+ runs on changing code | Test result history, mutation testing |
| Hides failures | Broad exception catch, stale baseline comparison | Code review, assertion audit |
| Coverage gap | Module lines uncovered despite assigned test | Istanbul, JaCoCo, Coverage.py |
Test result history over time is the most reliable signal for always-passing tests. A test that has never failed across 200 consecutive runs on a codebase receiving active commits should be examined. Not all of these are dead — stable utility functions and idempotent configuration validators can legitimately have long passing streaks — but any test covering a path with recent application changes and a 100% pass rate over that period warrants a targeted assertion review. Teams building a systematic approach to QA can review Astaqc’s manual testing services for context on where human assertion review fits within an automated suite.
Removing a test without understanding why it is dead creates a coverage regression if the test was the only thing covering a valid code path. The diagnostic step is to determine whether the test’s coverage intent is valid before deciding whether to fix the test or delete it.
For tests that never run, the first diagnostic question is whether the reason for skipping is still valid. Check the issue or comment referenced in the skip annotation. If the issue is closed, the blocker has been resolved and the test should be re-enabled and run against the current application state. If there is no referenced issue, the test was quarantined indefinitely without a resolution plan. Re-enable the test and allow it to run. If it fails, fix it. If it now passes, the quarantine was never cleaned up.
For tests that always pass, run mutation testing against the code paths they cover. Mutation testing — using tools like Stryker, Pitest, or mutmut — introduces controlled faults into the source code and verifies that the test suite detects them. A test that passes even when the mutation tool changes a conditional operator is not asserting on the value that distinguishes those cases. This is definitive evidence of an assertion gap.
For tests that hide failures, the diagnostic is a code review of the test body: does the final assertion evaluate the application state after the action, or does it evaluate the test infrastructure’s response? A test that calls an API endpoint and asserts that the request did not throw an exception is asserting on the request layer, not the application. If the endpoint changes behavior silently, this test continues to pass. The fix is to replace exception-absence assertions with explicit state assertions: check the response body, the database record, or the downstream effect.
Deletion without verification creates the same problem as the dead test: false confidence that the suite still covers what it did before. A safe deletion protocol takes three steps.
First, identify what the test was intended to cover. For unit tests, this is usually visible from the test name and the function under test. For integration and end-to-end tests, it requires reading the test body to understand the user flow or system behavior it was written to verify. If the intent cannot be reconstructed from the test itself, the test should not be deleted until the coverage intent is documented and either verified by an existing test or replaced by a new one.
Second, verify that the coverage intent is met by at least one other test. This is the step that teams skip when they treat dead-test removal as a cleanup task rather than a coverage migration. If a dead test is the only test covering a login edge case, removing it creates a real coverage gap even if the test itself was not executing correctly. Check the coverage report after removing the test to confirm that line coverage on the targeted module does not drop.
Third, remove the test in a separate commit from application changes, with a description that names the test, explains why it was removed, and references the coverage verification step. This makes the deletion traceable in git history and gives future engineers the context to understand why the suite does not contain a test for a particular code path.
Teams rebuilding a test suite after a dead-test purge often discover that the suite was larger in test count but smaller in actual coverage than they expected. Astaqc’s testing services include suite auditing and coverage gap analysis for teams that need structured support during this process. The outsourcing guide covers when it makes sense to bring in external QA support for this kind of work.
Search the codebase for skip annotations, xfail markers, and pending flags. For each one, trace the referenced issue or comment to determine whether the blocking condition has been resolved. A useful approach is to temporarily remove the skip and run the test — if it passes, the quarantine was outdated; if it fails, you have a real test failure to investigate rather than a silent gap in coverage.
Mutation testing identifies tests that would not catch controlled faults in their target code paths. It does not cover integration or end-to-end tests that test multiple code paths simultaneously, and it does not identify tests whose coverage intent is entirely absent from the production code. Mutation testing is the most reliable tool for unit tests; for integration tests, assertion audits and code review are more practical.
A quarterly dead-test audit is a reasonable baseline for actively developed codebases. The audit should cover skip count trends, test result history for always-passing tests, and coverage diff against the previous quarter. Teams with high deployment frequency may find value in a monthly audit, particularly if they are accumulating technical debt in the test suite from feature development sprints.
Coverage metrics measure line and branch execution, not assertion quality. A test can be removed without changing line coverage if another test happens to exercise the same lines for a different purpose. Verify that the remaining tests include at least one assertion on the specific behavior that the deleted test was intended to verify — not just that the lines execute, but that the output is evaluated.
A flaky test produces inconsistent results due to timing issues, environment dependencies, or non-deterministic behavior — it sometimes catches real failures and sometimes passes incorrectly. A dead test consistently produces results that do not reflect application correctness at all. Flaky tests need stabilization; dead tests need removal or replacement. The distinction matters because the remediation is different and the coverage risk of removing each type is different.
For teams that have identified widespread dead tests and need support rebuilding a reliable suite, Astaqc’s QA team, software testing services, and testing documentation services cover the full scope of test suite recovery and coverage improvement.
A CI pipeline reporting 400 passing tests provides false confidence if 60 of those tests have not executed their target code paths in months. Dead test removal is not cleanup — it is coverage verification.

Sign up to receive and connect to our newsletter