# 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.

- **Published:** 2026-06-09
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** solidity, testing, smart-contracts, checklist, quality-assurance
- **Canonical URL:** https://clixo.sh/blog/solidity-contract-testing-coverage-checklist

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.

```mermaid
flowchart LR
  A["Unit Tests"] --> B["Access Control Tests"]
  B --> C["Edge Case Tests"]
  C --> D["ETH & Token Handling"]
  D --> E["State Transition Tests"]
  E --> F["Fuzz Tests"]
  F --> G["Invariant Tests"]
  G --> H["Integration & Fork Tests"]
  H --> I["Gas Snapshot & CI"]
```

## 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](https://clixo.sh/services/smart-contract-development) can help you ship with confidence.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
