A software delivery pipeline can pass every build and test stage while still allowing important parts of an application to remain untested. A successful test run confirms that the existing tests passed. It does not necessarily confirm that the tests cover the code paths introduced or modified during development.
Code coverage helps teams understand how much application code is exercised by automated tests. However, checking coverage only during major releases or occasional quality reviews makes it difficult to identify when coverage starts declining.
Daily coverage checks provide a more consistent approach. They collect coverage data from regular builds, compare it with previous results, and identify unexpected changes before they become long-term quality problems.
For development teams using CI/CD, the objective is not to achieve 100% coverage. It is to maintain meaningful test coverage, detect untested changes, and make coverage information part of everyday delivery decisions.
Understand What Daily Coverage Checks Measure
Code coverage measures which parts of an application execute while automated tests are running.
Different coverage metrics provide different information.
Coverage metric | What it measures |
|---|
Line coverage | Percentage of executable source lines executed |
Statement coverage | Percentage of executable statements executed |
Branch coverage | Percentage of decision outcomes executed |
Function coverage | Percentage of functions or methods executed |
Condition coverage | Whether individual Boolean conditions evaluate to true and false |
Consider a simple method:
public decimal CalculateDiscount(decimal amount, bool isPremium)
{
if (amount <= 0)
{
throw new ArgumentException("Amount must be positive.");
}
if (isPremium)
{
return amount * 0.20m;
}
return amount * 0.05m;
}
A test that executes the method with a positive amount and isPremium set to false covers the normal non-premium path.
However, it does not execute the invalid amount condition or the premium discount path.
The method may show partial line coverage, but branch coverage reveals that some decision outcomes remain untested.
A meaningful coverage process should therefore consider more than one metric. Line coverage is useful for understanding execution, while branch coverage helps identify missing decision-path tests.
Coverage also has limitations. Executing a line does not prove that the test verifies the correct result. A test can execute an entire method without containing meaningful assertions.
Daily coverage checks should be treated as one quality signal alongside test results, mutation testing, static analysis, and code review.
Generate Coverage Reports From Automated Tests
The first step is to generate coverage data consistently.
For a .NET application, Coverlet is a commonly used coverage tool. It integrates with the .NET test ecosystem and can produce reports in formats supported by reporting platforms.
Consider a solution containing an application project and a test project:
OrderManagement/
OrderManagement.sln
src/
OrderManagement.Api/
OrderManagement.Core/
tests/
OrderManagement.Tests/
Install the Coverlet collector in the test project:
dotnet add tests/OrderManagement.Tests package coverlet.collector
Run the tests with coverage collection enabled:
dotnet test OrderManagement.sln \
--collect:"XPlat Code Coverage"
The test runner generates coverage information, commonly in Cobertura format, inside the test results directory.
To generate an HTML report that developers can inspect, install the ReportGenerator tool:
dotnet tool install -g dotnet-reportgenerator-globaltool
Generate the report:
reportgenerator \
"-reports:**/coverage.cobertura.xml" \
"-targetdir:coverage-report" \
"-reporttypes:Html"
The resulting report helps developers identify which classes, methods, and branches were exercised during testing.
For reliable daily comparisons, keep the test configuration consistent. Changing the included assemblies, test filters, or coverage exclusions can make one day's results incomparable with another day's results.
Schedule Daily Coverage Checks in CI/CD
Coverage collection should run automatically rather than depending on developers to execute special commands.
A scheduled GitHub Actions workflow can run the test suite every day.
For example:
name: Daily Code Coverage
on:
schedule:
- cron: "30 1 * * *"
workflow_dispatch:
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- name: Restore dependencies
run: dotnet restore
- name: Run tests with coverage
run: |
dotnet test OrderManagement.sln \
--configuration Release \
--collect:"XPlat Code Coverage"
- name: Generate coverage report
run: |
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator \
"-reports:**/coverage.cobertura.xml" \
"-targetdir:coverage-report" \
"-reporttypes:Html;Cobertura"
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: daily-coverage-report
path: coverage-report/
The .NET version is illustrative and should match the project's supported SDK.
The cron expression schedules the workflow according to UTC. Teams should account for the repository's preferred reporting time when configuring the schedule.
This workflow performs four main operations:
Restores the project dependencies.
Runs automated tests.
Generates a coverage report.
Stores the report as a build artifact.
Uploading the report makes it available for later inspection without requiring developers to rerun the pipeline.
A production implementation should also preserve the coverage summary in a machine-readable format so that a later job can compare it with previous results.
Compare Coverage With Previous Runs
Generating a report every day is useful, but it does not automatically identify coverage regressions.
A daily monitoring system needs a comparison mechanism.
For example, assume a project produces the following results:
Day | Line coverage | Branch coverage |
|---|
Monday | 82.5% | 74.2% |
Tuesday | 82.7% | 74.5% |
Wednesday | 82.6% | 74.4% |
Thursday | 79.8% | 69.1% |
Friday | 79.9% | 69.2% |
Thursday shows a significant reduction in both metrics.
The pipeline should flag the change and provide enough information to investigate it.
However, percentage changes need context. A reduction can happen because new code was introduced without tests, but it can also happen because a test was removed, a coverage filter changed, or a new project was included in the solution.
Store the following information with each coverage result:
This makes it possible to compare results from equivalent builds rather than comparing percentages without understanding how they were produced.
For teams maintaining several release branches, coverage history should be tracked separately for each branch.
Establish Coverage Thresholds Without Blocking Useful Changes
A common mistake is introducing a strict coverage threshold without understanding the existing application.
For example, setting a requirement of 90% coverage on a project currently operating at 65% may create a large number of blocked builds without providing an immediate improvement in test quality.
A more practical approach is to establish a baseline and gradually improve it.
A pipeline can use two types of thresholds:
Minimum overall coverage
This checks whether the entire application remains above an agreed baseline.
For example:
Minimum line coverage: 80%
Minimum branch coverage: 70%
These values are examples, not universal quality standards. The appropriate thresholds depend on the application, architecture, and testing strategy.
Coverage on changed code
This focuses on newly introduced or modified code.
A project might maintain an overall coverage baseline while requiring stronger coverage for important new business logic.
For example, a team could require tests for new payment calculations, authorization rules, or order-processing conditions without demanding that every legacy class immediately meet the same target.
Changed-code coverage is particularly useful in large applications because it avoids making every new feature responsible for historical coverage gaps.
Detect Coverage Regressions Automatically
A coverage comparison job can calculate the difference between the current report and the previous baseline.
For example, the following Python script demonstrates a simple comparison using two Cobertura reports:
import sys
import xml.etree.ElementTree as ET
def read_coverage(path):
root = ET.parse(path).getroot()
line_rate = float(root.attrib["line-rate"])
branch_rate = float(root.attrib["branch-rate"])
return line_rate * 100, branch_rate * 100
baseline = read_coverage(sys.argv[1])
current = read_coverage(sys.argv[2])
line_change = current[0] - baseline[0]
branch_change = current[1] - baseline[1]
print(f"Line coverage: {current[0]:.2f}%")
print(f"Branch coverage: {current[1]:.2f}%")
print(f"Line coverage change: {line_change:+.2f}")
print(f"Branch coverage change: {branch_change:+.2f}")
if line_change < -2.0 or branch_change < -2.0:
print("Coverage regression detected.")
sys.exit(1)
This example compares the overall line-rate and branch-rate values in two Cobertura reports.
The baseline and current reports must use equivalent coverage configurations. Otherwise, the comparison may produce misleading results.
For a production pipeline, the script should also validate the XML structure, handle missing reports, identify the correct baseline commit, and distinguish failed test execution from a genuine coverage regression.
The threshold should be configurable rather than hardcoded into the script.
Separate Test Failures From Coverage Failures
Coverage monitoring should not hide ordinary test failures.
Consider two different situations:
The test suite fails because an assertion is incorrect.
All tests pass, but coverage falls below the agreed threshold.
These are different quality conditions and should produce different pipeline messages.
A useful pipeline design has separate stages for:
Test execution.
Coverage report generation.
Coverage comparison.
Quality-gate evaluation.
Report publication.
If tests fail, the coverage report may be incomplete or unavailable. The pipeline should not interpret missing coverage as zero coverage without explaining the actual failure.
Similarly, a report-generation failure should not be presented as a coverage regression.
This separation makes CI results easier to understand and reduces unnecessary investigation.
Make Daily Coverage Results Useful to Developers
A percentage displayed in a dashboard is not enough to improve testing practices.
The report should identify the areas responsible for a coverage decline.
For example, instead of reporting only:
Line coverage dropped from 82.5% to 79.8%.
Provide additional information:
Coverage regression detected.
Affected project: OrderManagement.Core
Affected class: DiscountCalculator
Uncovered method: CalculateDiscount
Uncovered branches:
- Invalid amount
- Premium customer discount
This helps developers identify which tests need attention.
Daily reports can also be sent to the development team through an existing notification platform. However, notifications should be triggered by meaningful changes rather than every successful daily run.
A practical notification policy might include:
Coverage falls below the agreed baseline.
Branch coverage drops significantly.
A new project has no coverage report.
A critical business module loses test coverage.
Coverage generation fails repeatedly.
Avoid creating alerts for small fluctuations that do not require action.
Common Mistakes in Daily Coverage Monitoring
Mistake | Why it creates problems | Better approach |
|---|
Measuring only line coverage | Untested decision paths may remain hidden | Track branch coverage as well |
Targeting 100% coverage immediately | Encourages unnecessary or low-value tests | Set realistic, risk-based thresholds |
Comparing different test configurations | Produces misleading trends | Keep coverage settings consistent |
Ignoring changed-code coverage | New code can reduce quality while overall coverage remains high | Measure coverage on modified code |
Treating every decline as a failure | Some changes result from legitimate project or filter updates | Investigate the source of the change |
Running tests without storing reports | Historical trends cannot be analyzed | Retain machine-readable coverage artifacts |
Treating coverage as proof of correctness | Executed code may still have incorrect behavior | Combine coverage with assertions and other testing techniques |
Conclusion
Daily coverage checks make test quality more visible throughout the software delivery lifecycle. They help teams identify declining coverage, find untested code paths, and detect changes before they become difficult to address.
For an effective implementation, generate coverage reports automatically, preserve historical results, compare equivalent builds, and apply thresholds that reflect application risk. Changed-code coverage is particularly useful because it encourages teams to test new functionality without requiring an immediate rewrite of legacy test suites.
Most importantly, coverage should guide testing decisions rather than become a target that developers optimize blindly. A smaller suite of meaningful tests can provide more confidence than a larger suite that executes code without verifying its behavior.
Join the conversation! Your thoughts help the community grow.