Test coverage analysis and improvement strategies for Solidity contracts. Use when analyzing or improving test coverage.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill provides strategies for analyzing and improving test coverage in Solidity projects.
Use this skill when:
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
# 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
# Install
npm install --save-dev solidity-coverage
# Generate report
npx hardhat coverage
# Output in coverage/index.html
Foundry lcov format:
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:
// Often uncovered: revert branches
function transfer(uint256 amount) public {
if (balance < amount) {
revert InsufficientBalance(); // ⚠️ Test this path!
}
// Main path is usually covered
}
// 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
}
// Ensure all modifier paths covered
modifier onlyOwner() {
require(msg.sender == owner); // ⚠️ Test unauthorized access
_;
}
// May be uncovered if only called conditionally
function _internalCalc() private returns (uint256) {
// ⚠️ Ensure all code paths call this
}
// 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
// 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 {}
// 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 {}
// For each restricted function, test:
function test_Admin_CanPause() public {} // ✅ Authorized
function test_RevertWhen_NonAdminPauses() public {} // ✅ Unauthorized
# 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
High Priority (Must cover):
Medium Priority (Should cover):
Low Priority (Nice to have):
// 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 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 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
}
// solhint-disable-next-line
function unreachableCode() private pure {
// Intentionally never called
// Could be legacy code or future feature
}
// test/harness/MyContractHarness.sol
// Don't count test helpers in coverage
contract MyContractHarness is MyContract {
function exposed_internalFunction() public {
return _internalFunction();
}
}
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
// ❌ 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);
}
// 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!
}
| 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.