WritingSolidity Contract Testing Coverage Checklist Before You Ship — Clixo
5 min readsolidity, testing, smart-contracts, checklist, quality-assurance

Solidity Contract Testing Coverage Checklist Before You Ship

A structured checklist for Solidity smart contract test coverage — unit tests, edge cases, access control, fuzz testing, and invariants before mainnet deployment.

Most Solidity bugs that reach mainnet were testable before deployment. The problem is not that developers do not write tests — it is that they write the wrong tests. Happy-path coverage gives false confidence. Production contracts need structured coverage that thinks adversarially, not optimistically.

Use this checklist before any smart contract system goes to mainnet.

Solidity Smart Contract Testing Coverage Checklist

Unit Tests — Function-Level Coverage

  • Every public and external function has at least one unit test
  • Every require and revert condition has a test that triggers it
  • Every event emission is verified (event args, not just that the function did not revert)
  • Return values are explicitly asserted — do not just check that a call succeeds
  • Functions with multiple code paths (if/else, loops) have tests covering each branch

Access Control Tests

  • Every onlyOwner, onlyRole, or custom modifier is tested with an unauthorized caller
  • The unauthorized call reverts with the expected error
  • Role grant and revoke functions work correctly and emit expected events
  • renounceOwnership or equivalent is tested — and the contract behaves correctly after the call
  • Admin functions are inaccessible to regular users

Edge Case Tests

  • Boundary values for numeric inputs (0, max uint256, values near overflow)
  • Empty arrays and single-element arrays for array parameters
  • Zero address inputs for address parameters
  • Calling functions in unexpected order (if your contract has stateful workflows)
  • Functions called multiple times in sequence

ETH and Token Handling

  • Receiving ETH behaves correctly (if your contract has receive() or payable functions)
  • Sending ETH to non-payable contracts reverts
  • ERC-20 transfers with tokens that return false on failure (non-reverting tokens)
  • ERC-20 transfers with fee-on-transfer tokens if your contract interacts with arbitrary tokens
  • Reentrancy scenarios for any function that sends ETH or calls external contracts

State Transition Tests

  • State changes persist correctly across calls
  • Multiple users interacting in sequence produce correct state
  • State cannot transition backwards when it should not be able to
  • Paused or frozen contract correctly rejects calls when paused

Fuzz Tests

  • At least one fuzz test per critical numeric function
  • Fuzz inputs include the full type range (not artificially bounded unless invariant requires it)
  • vm.assume() is used sparingly — over-constraining fuzz inputs defeats the purpose
  • Gas consumption under fuzz inputs does not spike unexpectedly

Invariant Tests

  • Identify the core invariants of your system (e.g., total supply equals sum of balances)
  • Write at least one stateful invariant test using Foundry's invariant testing or Echidna
  • Invariants hold across all sequences of function calls, not just the happy path

Integration and Fork Tests

  • If your contract interacts with on-chain protocols (Uniswap, Aave, Chainlink), test against a mainnet fork using vm.createFork()
  • Oracle values and price feeds are tested under stale and manipulated conditions
  • Deployment script is tested on a fork before testnet deployment

Gas Tests

  • Run forge snapshot and store the baseline
  • Gas snapshot is committed to the repo and compared in CI
  • No function costs significantly more gas than expected for its operation
  • High-frequency functions (called per user interaction) are profiled specifically

Common Testing Gaps That Lead to Exploits

Testing only expected inputs. Real attackers pass unexpected inputs. Your tests should include values no normal user would send.

No tests for the deployer losing keys. Multi-sig ownership, role transitions, and emergency recovery paths are often untested because they feel hypothetical. They are not.

No multi-user tests. Many bugs only appear when two users interact with the same contract state simultaneously. Use Foundry's cheatcodes to simulate multiple actors in sequence.

Ignoring gas limits. A function that processes an unbounded array can run out of gas at scale. Test with realistic data sizes.

Running Your Tests

In Foundry:

forge test -vvv              # verbose output with traces
forge coverage               # line-level coverage report
forge snapshot               # gas snapshot per test
forge test --fuzz-runs 10000 # increase fuzz iterations

Aim for meaningful coverage — coverage percentage is a useful metric but not a safety guarantee on its own. A test suite with 95% coverage but no access control tests is weaker than one with 80% coverage that tests all privilege boundaries.

If your contract handles real funds and you want an engineering team that builds test coverage like this by default, Clixo can help you ship with confidence.