Coverage Analysis Skill
This skill provides strategies for analyzing and improving test coverage in Solidity projects.
When to Use
Use this skill when:
- •Analyzing test coverage
- •Improving coverage metrics
- •Identifying untested code paths
- •Preparing for audits
- •Establishing coverage requirements
Coverage Metrics
Types of Coverage
Line Coverage: Percentage of code lines executed Branch Coverage: Percentage of decision branches taken Function Coverage: Percentage of functions called Statement Coverage: Percentage of statements executed
Coverage Goals
- •Line Coverage: >95%
- •Branch Coverage: >90%
- •Function Coverage: 100%
- •Critical Paths: 100%
Measuring Coverage
Foundry
bash
# Generate coverage report forge coverage # Detailed report forge coverage --report lcov genhtml lcov.info --output-directory coverage open coverage/index.html # Debug coverage forge coverage --report debug > coverage.txt # Coverage for specific contracts forge coverage --match-contract MyContract
Hardhat
bash
# Install npm install --save-dev solidity-coverage # Generate report npx hardhat coverage # Output in coverage/index.html
Identifying Uncovered Code
Reading Coverage Reports
Foundry lcov format:
code
File | % Lines | % Statements | % Branches | % Funcs | ----------------------|--------------|--------------|--------------|--------------| src/MyContract.sol | 95.00% (38/40)| 95.00% (38/40)| 87.50% (7/8) | 100.00% (5/5)|
Areas to investigate:
- •Lines with 0 executions
- •Branches not taken (if/else, require, etc.)
- •Functions never called
- •Error paths not triggered
Common Uncovered Areas
- •Error Handlers
solidity
// Often uncovered: revert branches
function transfer(uint256 amount) public {
if (balance < amount) {
revert InsufficientBalance(); // ⚠️ Test this path!
}
// Main path is usually covered
}
- •Edge Cases
solidity
// Test boundary conditions
function withdraw(uint256 amount) public {
require(amount > 0); // ⚠️ Test with 0
require(amount <= balance); // ⚠️ Test with max
require(amount <= type(uint256).max); // ⚠️ Test overflow
}
- •Modifiers
solidity
// Ensure all modifier paths covered
modifier onlyOwner() {
require(msg.sender == owner); // ⚠️ Test unauthorized access
_;
}
- •Internal/Private Functions
solidity
// May be uncovered if only called conditionally
function _internalCalc() private returns (uint256) {
// ⚠️ Ensure all code paths call this
}
Improving Coverage
1. Test All Branches
solidity
// Contract function
function withdraw(uint256 amount) public {
if (amount > balance) {
revert InsufficientBalance();
}
balance -= amount;
}
// Tests needed
function test_Withdraw_Success() public {} // ✅ Success path
function test_RevertWhen_InsufficientBalance() public {} // ✅ Revert path
2. Test Edge Cases
solidity
// Test suite
function test_Transfer_ZeroAmount() public {}
function test_Transfer_MaxAmount() public {}
function test_Transfer_ToZeroAddress() public {}
function test_Transfer_ToSelf() public {}
function test_Transfer_WithNoBalance() public {}
3. Test All Function Combinations
solidity
// Contract with state-dependent behavior
contract Stateful {
enum State { Pending, Active, Closed }
State public state;
function action() public {
if (state == State.Pending) { /* ... */ }
else if (state == State.Active) { /* ... */ }
else { /* ... */ }
}
}
// Test each state
function test_Action_WhenPending() public {}
function test_Action_WhenActive() public {}
function test_Action_WhenClosed() public {}
4. Test Access Control Paths
solidity
// For each restricted function, test:
function test_Admin_CanPause() public {} // ✅ Authorized
function test_RevertWhen_NonAdminPauses() public {} // ✅ Unauthorized
Coverage Gaps Analysis
Finding Gaps
bash
# Foundry: Show uncovered lines forge coverage --report debug | grep -A 5 "0 hits" # Hardhat: Open HTML report npx hardhat coverage # Check coverage/index.html for red/yellow lines
Prioritizing Coverage
High Priority (Must cover):
- •Fund transfer functions
- •Access control checks
- •State-changing operations
- •External call handling
- •Error conditions
Medium Priority (Should cover):
- •View functions with logic
- •Internal calculations
- •Event emissions
- •Modifier logic
Low Priority (Nice to have):
- •Simple getters
- •Pure utility functions
- •Unreachable code (if any)
Coverage Strategies
Fuzz Testing for Coverage
solidity
// Fuzz tests automatically cover many paths
function testFuzz_Transfer(address to, uint256 amount) public {
vm.assume(to != address(0));
vm.assume(amount <= type(uint128).max);
// Foundry will test with many random values
// Increases branch coverage automatically
}
Invariant Testing for Coverage
solidity
// Invariant tests exercise many code paths
contract InvariantTest is Test {
function invariant_SumOfBalancesEqualsTotalSupply() public {
// This gets called after random state changes
// Increases coverage naturally
}
}
Integration Tests
solidity
// Integration tests cover cross-contract interactions
function test_FullWorkflow() public {
token.approve(address(vault), 1000);
vault.deposit(1000);
vm.warp(block.timestamp + 30 days);
vault.withdraw();
// Covers multiple contracts and functions
}
Excluding Code from Coverage
Unreachable Code
solidity
// solhint-disable-next-line
function unreachableCode() private pure {
// Intentionally never called
// Could be legacy code or future feature
}
Test Harnesses
solidity
// test/harness/MyContractHarness.sol
// Don't count test helpers in coverage
contract MyContractHarness is MyContract {
function exposed_internalFunction() public {
return _internalFunction();
}
}
CI/CD Coverage Requirements
GitHub Actions Example
yaml
name: Coverage Check
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run coverage
run: forge coverage --report summary
- name: Check coverage threshold
run: |
COVERAGE=$(forge coverage --report summary | grep "Total" | awk '{print $4}' | sed 's/%//')
if (( $(echo "$COVERAGE < 95" | bc -l) )); then
echo "Coverage $COVERAGE% is below threshold of 95%"
exit 1
fi
Coverage Anti-Patterns
❌ Testing for Coverage, Not Correctness
solidity
// ❌ Bad: Just calling functions for coverage
function test_Transfer() public {
token.transfer(user, 100); // No assertions!
}
// ✅ Good: Testing actual behavior
function test_Transfer_UpdatesBalances() public {
uint256 senderBefore = token.balanceOf(sender);
uint256 recipientBefore = token.balanceOf(recipient);
vm.prank(sender);
token.transfer(recipient, 100);
assertEq(token.balanceOf(sender), senderBefore - 100);
assertEq(token.balanceOf(recipient), recipientBefore + 100);
}
❌ 100% Coverage ≠ No Bugs
solidity
// Even with 100% coverage, logic bugs can exist
function calculateReward(uint256 stake) public pure returns (uint256) {
return stake * 2; // Should be stake * 110 / 100 (10% reward)
}
// Test has 100% coverage but doesn't catch the bug
function test_CalculateReward() public {
uint256 reward = calculateReward(100);
assert(reward > 0); // Passes but logic is wrong!
}
Coverage Best Practices
- •Aim for high coverage, not 100% - Some code paths may be intentionally unreachable
- •Focus on critical paths first - Fund transfers, access control, state changes
- •Use fuzz and invariant tests - Automatically increase branch coverage
- •Test error conditions - Reverts and edge cases
- •Document excluded code - Explain why certain code isn't covered
- •Review coverage reports regularly - Make coverage part of code review
- •Set minimum thresholds - Enforce in CI/CD
Quick Reference
| Framework | Generate Coverage | View Report |
|---|---|---|
| Foundry | forge coverage | forge coverage --report debug |
| Hardhat | npx hardhat coverage | Open coverage/index.html |
| Metric | Minimum | Target |
|---|---|---|
| Line Coverage | 90% | 95% |
| Branch Coverage | 85% | 90% |
| Function Coverage | 95% | 100% |
Remember: Coverage is a necessary but not sufficient condition for quality. High coverage with poor test quality is worse than lower coverage with rigorous tests. Focus on meaningful test scenarios, not just hitting coverage numbers.