Skip to content
Back to skills

Smart Contract Testing

ASecurity

Guide for testing smart contracts using Foundry and Hardhat, covering unit tests, fuzz testing, invariant testing, fork testing, and gas benchmarking.

  • 7 stars
  • 0 votes
  • 0 copies
  • 8 views
  • Added June 7, 2026
testingjavascripttypescriptgojavabashtestingdebuggingapisecurity

Works with

  • api

Security analysis

A100/100

Scanned June 7, 2026

npx -y skills add nirholas/three-ui --skill smart-contract-testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Smart Contract Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Smart Contract Testing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nirholas-smart-contract-testing/badge)](https://www.skillsdirectory.com/skills/nirholas-smart-contract-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: smart-contract-testing
description: Guide for testing smart contracts using Foundry and Hardhat, covering unit tests, fuzz testing, invariant testing, fork testing, and gas benchmarking.
license: MIT
metadata:
  category: development
  difficulty: intermediate
  author: sperax-team
  tags: [development, testing, foundry, hardhat, smart-contracts]
---

# Smart Contract Testing

## When to use this skill

Use when the user asks about:
- Writing tests for smart contracts
- Setting up a testing framework (Foundry or Hardhat)
- Fuzz testing or invariant testing strategies
- Fork testing against mainnet state
- Gas benchmarking and optimization verification
- Debugging failing contract tests

## Testing Framework Selection

### Foundry vs Hardhat

| Feature | Foundry | Hardhat |
|---------|---------|---------|
| Language | Solidity | JavaScript/TypeScript |
| Speed | Very fast (native) | Slower (JS runtime) |
| Fuzzing | Built-in | Requires plugins |
| Fork testing | Built-in | Built-in |
| Debugging | forge debug, traces | console.sol, traces |
| Ecosystem | Growing rapidly | Mature, large plugin ecosystem |

Recommendation: Use Foundry for new projects. Use Hardhat when you need complex JS scripting or specific Hardhat plugins.

## Testing with Foundry

### 1. Unit Test Structure

Follow this pattern for each test:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {MyContract} from "../src/MyContract.sol";

contract MyContractTest is Test {
    MyContract internal target;
    address internal alice = makeAddr("alice");
    address internal bob = makeAddr("bob");

    function setUp() public {
        target = new MyContract();
        deal(alice, 100 ether);
    }

    function test_deposit_succeeds() public {
        vm.prank(alice);
        target.deposit{value: 1 ether}();
        assertEq(target.balanceOf(alice), 1 ether);
    }

    function test_deposit_revertsWhenZero() public {
        vm.prank(alice);
        vm.expectRevert(MyContract.ZeroAmount.selector);
        target.deposit{value: 0}();
    }
}
```

Naming conventions:
- `test_functionName_scenario` for expected-pass tests
- `test_functionName_revertsWhen_condition` for expected-revert tests
- `testFuzz_functionName` for fuzz tests

### 2. Fuzz Testing

Let Foundry generate random inputs to find edge cases:

```solidity
function testFuzz_deposit(uint256 amount) public {
    // Bound the input to realistic ranges
    amount = bound(amount, 1, 100 ether);
    
    deal(alice, amount);
    vm.prank(alice);
    target.deposit{value: amount}();
    
    assertEq(target.balanceOf(alice), amount);
}
```

Best practices for fuzz tests:
- Use `bound()` to constrain inputs to valid ranges
- Use `vm.assume()` sparingly — prefer `bound()` to avoid too many rejected runs
- Set meaningful fuzz runs in `foundry.toml`: `[fuzz] runs = 1000`
- Focus fuzz testing on functions with numerical inputs and complex math

### 3. Invariant Testing

Define properties that must always hold regardless of actions:

```solidity
contract MyContractInvariant is Test {
    MyContract internal target;
    Handler internal handler;

    function setUp() public {
        target = new MyContract();
        handler = new Handler(target);
        targetContract(address(handler));
    }

    function invariant_totalSupplyMatchesBalances() public view {
        assertEq(
            target.totalSupply(),
            target.balanceOf(address(handler))
        );
    }
}
```

Common invariants to test:
- Total supply equals sum of all balances
- Contract balance always covers total deposits
- Exchange rates are monotonically non-decreasing
- Access control roles cannot be self-granted

### 4. Fork Testing

Test against real mainnet state:

```solidity
function test_swapOnUniswap_fork() public {
    // Fork mainnet at a specific block
    vm.createSelectFork("mainnet", 19_000_000);
    
    address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    
    deal(usdc, alice, 10_000e6);
    
    vm.startPrank(alice);
    // ... execute swap logic
    vm.stopPrank();
    
    assertGt(IERC20(weth).balanceOf(alice), 0);
}
```

Fork testing tips:
- Pin to a specific block number for reproducibility
- Use `deal()` to set up token balances without needing to acquire them
- Test against multiple chains by configuring RPC URLs in `foundry.toml`
- Be mindful of rate limits on RPC providers

### 5. Gas Benchmarking

Track gas usage to catch regressions:

```bash
# Generate gas snapshot
forge snapshot

# Compare against previous snapshot
forge snapshot --check
```

In tests, measure specific operations:
```solidity
function test_deposit_gasUsage() public {
    vm.prank(alice);
    uint256 gasBefore = gasleft();
    target.deposit{value: 1 ether}();
    uint256 gasUsed = gasBefore - gasleft();
    
    // Assert gas is within expected range
    assertLt(gasUsed, 50_000, "deposit uses too much gas");
}
```

### 6. Coverage and CI

Ensure comprehensive test coverage:
- Run `forge coverage` to identify uncovered code paths
- Target 100% line coverage on security-critical functions
- Set up CI to run tests on every PR
- Include gas snapshot comparison in CI to catch regressions
- Run fuzz tests with higher iterations in CI (10,000+) vs local development (256)

### 7. Debugging Techniques

When tests fail:
- Use `forge test -vvvv` for full execution traces
- Add `console.log()` via `import "forge-std/console.sol";`
- Use `forge debug` for step-by-step EVM execution
- Check event emissions with `vm.expectEmit()`
- Verify storage values with `vm.load(address, slot)`

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.