Code Coverage
Code coverage measures how much of your source code is executed when running tests. It helps identify untested code paths and ensures your test suite exercises the important parts of your application.
Quick Start
Enable coverage tracking with the --coverage flag:
bashunit tests/ --coveragebashunit tests/ --coverage-paths src/bashunit - 0.30.0 | Tests: 5
.....
Tests: 5 passed, 5 total
Assertions: 12 passed, 12 total
All tests passed
Time taken: 1 s
Coverage Report
---------------
src/math.sh 2/ 3 lines ( 66%)
src/utils.sh 8/ 10 lines ( 80%)
---------------
Total: 10/13 (76%)
Coverage report written to: coverage/lcov.infoHow It Works
bashunit uses Bash's built-in DEBUG trap mechanism to track line execution:
- Trap Setup: When coverage is enabled, a DEBUG trap is set that fires before every command execution
- Line Recording: Each executed line's file path and line number are recorded
- Filtering: Only files matching your coverage paths (and not excluded) are tracked
- Aggregation: After tests complete, hit data is aggregated and reported
Performance
The DEBUG trap adds overhead to test execution. For large test suites, consider running coverage periodically rather than on every test run.
Configuration
Command Line Options
| Option | Description |
|---|---|
--coverage | Enable code coverage tracking |
--coverage-paths <paths> | Comma-separated paths to track (default: src/) |
--coverage-exclude <patterns> | Comma-separated exclusion patterns |
--coverage-report <file> | LCOV report output path (default: coverage/lcov.info) |
--coverage-report-html <dir> | Generate HTML coverage report with line-by-line details |
--coverage-min <percent> | Minimum coverage threshold (fails if below) |
--no-coverage-report | Disable LCOV file generation (console only) |
Auto-enable
Coverage is automatically enabled when using --coverage-report, --coverage-report-html, or --coverage-min. You don't need to specify --coverage explicitly with these options.
Environment Variables
You can also configure coverage via environment variables in your .env file:
# Enable coverage
BASHUNIT_COVERAGE=true
# Paths to track (comma-separated)
BASHUNIT_COVERAGE_PATHS=src/,lib/
# Patterns to exclude (comma-separated)
BASHUNIT_COVERAGE_EXCLUDE=tests/*,vendor/*,*_test.sh
# LCOV report output path
BASHUNIT_COVERAGE_REPORT=coverage/lcov.info
# HTML report output directory (generates line-by-line coverage view)
BASHUNIT_COVERAGE_REPORT_HTML=coverage/html
# Minimum coverage percentage (optional)
BASHUNIT_COVERAGE_MIN=80
# Color thresholds for console output
BASHUNIT_COVERAGE_THRESHOLD_LOW=50 # Red below this
BASHUNIT_COVERAGE_THRESHOLD_HIGH=80 # Green above this, yellow betweenExamples
Basic Coverage
Track coverage for the default src/ directory:
bashunit tests/ --coverageCustom Source Paths
Track multiple directories:
bashunit tests/ --coverage --coverage-paths "src/,lib/,bin/"BASHUNIT_COVERAGE_PATHS=src/,lib/,bin/Exclusion Patterns
Exclude specific files or directories:
bashunit tests/ --coverage --coverage-exclude "vendor/*,*_mock.sh,deprecated/"BASHUNIT_COVERAGE_EXCLUDE=vendor/*,*_mock.sh,deprecated/Setting Minimum Threshold
Fail the test run if coverage drops below a threshold:
bashunit tests/ --coverage-min 80Coverage Report
---------------
src/math.sh 10/ 12 lines ( 83%)
---------------
Total: 10/12 (83%)Coverage Report
---------------
src/math.sh 5/ 12 lines ( 41%)
---------------
Total: 5/12 (41%)
Coverage 41% is below minimum 80%Console-Only Output
Skip generating the LCOV file:
bashunit tests/ --coverage --no-coverage-reportHTML Coverage Report
Generate a detailed HTML report showing line-by-line coverage:
bashunit tests/ --coverage-report-html coverage/htmlBASHUNIT_COVERAGE_REPORT_HTML=coverage/htmlThis creates a directory with:
index.html- Summary page with per-file coverage percentagesfiles/*.html- Individual source file views with line highlighting
Line highlighting:
- Green background: Lines executed during tests (covered)
- Red background: Executable lines not executed (uncovered)
- No background: Non-executable lines (comments, function declarations, etc.)
Each line also shows the number of times it was executed, helping identify hot paths and dead code.
CI/CD Integration
Generate coverage for CI tools like Codecov or Coveralls:
- name: Run tests with coverage
run: bashunit tests/ --coverage-min 80
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
fail_ci_if_error: truetest:
script:
- bashunit tests/ --coverage
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/lcov.infoUnderstanding the Console Report
The console report shows coverage per file with color coding:
Coverage Report
---------------
src/math.sh 10/ 12 lines ( 83%) # Green (>= 80%)
src/parser.sh 7/ 10 lines ( 70%) # Yellow (50-79%)
src/legacy.sh 2/ 15 lines ( 13%) # Red (< 50%)
---------------
Total: 19/37 (51%)Color thresholds (configurable via environment variables):
- Green: Coverage >= 80% (
BASHUNIT_COVERAGE_THRESHOLD_HIGH) - Yellow: Coverage 50-79%
- Red: Coverage < 50% (
BASHUNIT_COVERAGE_THRESHOLD_LOW)
Understanding LCOV Format
The coverage/lcov.info file uses the industry-standard LCOV format, compatible with most CI coverage tools.
File Structure
TN:
SF:/path/to/source/file.sh
DA:2,5
DA:3,0
LF:2
LH:1
end_of_recordField Reference
| Field | Description | Example |
|---|---|---|
TN: | Test Name (usually empty) | TN: |
SF: | Source File path | SF:/home/user/project/src/math.sh |
DA: | Line Data: line_number,hit_count | DA:15,3 (line 15 hit 3 times) |
LF: | Lines Found (total executable lines) | LF:25 |
LH: | Lines Hit (lines with hits > 0) | LH:20 |
end_of_record | Marks end of file entry | end_of_record |
Example Breakdown
Given this source file src/math.sh:
#!/usr/bin/env bash # Line 1 - not executable (comment/shebang)
function add() { # Line 2 - not executable (function declaration)
echo $(($1 + $2)) # Line 3 - executable
} # Line 4 - not executable (closing brace)
function multiply() { # Line 5 - not executable (function declaration)
echo $(($1 * $2)) # Line 6 - executable
} # Line 7 - not executable (closing brace)If tests call add twice but never call multiply, the LCOV output would be:
TN:
SF:/path/to/src/math.sh
DA:3,2
DA:6,0
LF:2
LH:1
end_of_recordInterpretation:
- Line 3 (
addbody): 2 hits - Line 6 (
multiplybody): 0 hits - 2 executable lines found, 1 line was hit (50% coverage)
Parallel Execution
Coverage works seamlessly with parallel test execution (-p flag):
bashunit tests/ --coverage -pHow it works:
- Each parallel worker writes to its own coverage file
- After all tests complete, coverage data is aggregated
- The final report combines hits from all workers
TIP
Coverage percentages should be identical whether running in parallel or sequential mode.
What Gets Tracked
Executable Lines
bashunit counts these as executable lines:
- Commands and statements
- Single-line function bodies (
function foo() { echo "hi"; })
Non-Executable Lines (Skipped)
These lines are not counted toward coverage:
- Empty lines
- Comment lines (including shebang
#!/usr/bin/env bash) - Function declaration lines (
function foo() {) - Lines with only braces (
{or})
Limitations
External Commands
Coverage only tracks Bash code. External commands (like grep, sed, etc.) are not tracked, though the lines that call them are.
Subshell Behavior
Due to Bash's process model, some subshell contexts may not have full coverage tracking. The DEBUG trap is inherited into subshells, but complex nested scenarios may have edge cases.