Tutorial·10 मिनट का पठन·Solingo द्वारा

Smart Contract Testing with Foundry — Complete Guide

Foundry के साथ comprehensive smart contract testing: unit tests, fuzz testing, invariant testing और gas optimization।

# Smart Contract Testing with Foundry — Complete Guide

Testing smart contracts critical है क्योंकि bugs costly हैं और code immutable है। Foundry Rust में written modern testing framework है जो extreme speed और powerful features offer करता है।

इस guide में, हम cover करेंगे unit testing, fuzz testing, invariant testing और best practices।

Why Foundry?

Foundry के advantages Hardhat के over:

  • 10-100x faster test execution
  • Solidity में tests (no JavaScript context switching)
  • Native fuzz testing
  • Invariant testing built-in
  • Gas profiling per function
  • Cheatcodes for blockchain manipulation

Setup

# Install Foundry

curl -L https://foundry.paradigm.xyz | bash

foundryup

# Create project

forge init my-project

cd my-project

# Project structure

# ├── src/ # Contracts

# ├── test/ # Tests

# ├── script/ # Deployment scripts

# └── foundry.toml # Configuration

1. Unit Testing

Basic test structure:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import "forge-std/Test.sol";

import "../src/Token.sol";

contract TokenTest is Test {

Token token;

address alice = address(0x1);

address bob = address(0x2);

// Setup runs before each test

function setUp() public {

token = new Token("MyToken", "MTK", 1000000);

}

function testInitialSupply() public {

assertEq(token.totalSupply(), 1000000 * 10**18);

}

function testTransfer() public {

uint256 amount = 100 * 10**18;

token.transfer(alice, amount);

assertEq(token.balanceOf(alice), amount);

assertEq(

token.balanceOf(address(this)),

1000000 * 10**18 - amount

);

}

function testTransferFailsInsufficientBalance() public {

vm.expectRevert("Insufficient balance");

token.transfer(alice, 2000000 * 10**18);

}

function testApproveAndTransferFrom() public {

uint256 amount = 500 * 10**18;

token.approve(alice, amount);

vm.prank(alice);

token.transferFrom(address(this), bob, amount);

assertEq(token.balanceOf(bob), amount);

}

}

Running Tests

# Run all tests

forge test

# Verbose output

forge test -vv

# Very verbose (logs)

forge test -vvv

# Trace (full execution)

forge test -vvvv

# Specific test

forge test --match-test testTransfer

# Specific contract

forge test --match-contract TokenTest

# Gas report

forge test --gas-report

2. Cheatcodes

Foundry cheatcodes blockchain manipulate करने के लिए powerful commands हैं।

Common Cheatcodes:

contract CheatcodeTest is Test {

function testPrank() public {

address alice = address(0x1);

// Next call से alice भेजा जाएगा

vm.prank(alice);

token.transfer(bob, 100);

// Verify caller was alice

}

function testStartPrank() public {

// सभी subsequent calls से alice भेजे जाएंगे

vm.startPrank(alice);

token.approve(bob, 100);

token.transfer(bob, 50);

vm.stopPrank();

}

function testDeal() public {

// Alice को 10 ETH दें

vm.deal(alice, 10 ether);

assertEq(alice.balance, 10 ether);

}

function testWarp() public {

// Time को 1 day आगे बढ़ाएं

vm.warp(block.timestamp + 1 days);

}

function testRoll() public {

// Block number आगे बढ़ाएं

vm.roll(block.number + 100);

}

function testExpectRevert() public {

vm.expectRevert("Insufficient balance");

token.transfer(alice, 9999999 ether);

}

function testExpectEmit() public {

// Expect specific event

vm.expectEmit(true, true, false, true);

emit Transfer(address(this), alice, 100);

token.transfer(alice, 100);

}

function testMockCall() public {

address oracle = address(0x123);

// Mock oracle response

vm.mockCall(

oracle,

abi.encodeWithSelector(IOracle.getPrice.selector),

abi.encode(3000e18) // $3000

);

// अब oracle.getPrice() 3000 return करेगा

}

}

3. Fuzz Testing

Fuzz testing random inputs generate करता है edge cases find करने के लिए।

contract FuzzTest is Test {

Token token;

function setUp() public {

token = new Token("Test", "TST", 1000000);

}

// Foundry random uint256 values pass करेगा

function testFuzz_transfer(uint256 amount) public {

// Assumptions: input को constrain करें

vm.assume(amount > 0);

vm.assume(amount <= token.totalSupply());

address recipient = address(0x1);

token.transfer(recipient, amount);

assertEq(token.balanceOf(recipient), amount);

}

// Multiple parameters

function testFuzz_transferBetweenUsers(

address from,

address to,

uint256 amount

) public {

vm.assume(from != address(0));

vm.assume(to != address(0));

vm.assume(from != to);

vm.assume(amount > 0 && amount <= 1000000 * 10**18);

// Give tokens to from

vm.prank(address(this));

token.transfer(from, amount);

// Transfer from -> to

vm.prank(from);

token.transfer(to, amount);

assertEq(token.balanceOf(to), amount);

}

// Bounded inputs

function testFuzz_transferBounded(uint128 amount) public {

// uint128 automatically bounds input

vm.assume(amount > 0);

token.transfer(address(0x1), amount);

// Test logic

}

}

Configure Fuzz Runs:

# foundry.toml

[fuzz]

runs = 10000 # Number of test cases (default: 256)

max_test_rejects = 100 # Max rejections before giving up

seed = "0x123" # Seed for reproducibility

4. Invariant Testing

Invariant testing verify करता है कि properties always true रहती हैं।

contract InvariantTest is Test {

Token token;

Handler handler;

function setUp() public {

token = new Token("Test", "TST", 1000000);

handler = new Handler(token);

// Target handler for invariant testing

targetContract(address(handler));

}

// यह invariant हमेशा true होनी चाहिए

function invariant_totalSupplyConstant() public {

assertEq(

token.totalSupply(),

1000000 * 10**18,

"Total supply changed!"

);

}

function invariant_sumOfBalances() public {

// Sum of all balances = total supply

uint256 sum = handler.sumOfBalances();

assertEq(sum, token.totalSupply());

}

function invariant_noNegativeBalance() public {

// कोई भी balance negative नहीं हो सकता (uint256 से already impossible)

// लेकिन custom checks add कर सकते हैं

}

}

// Handler: random actions perform करता है

contract Handler is Test {

Token token;

address[] public actors;

constructor(Token _token) {

token = _token;

}

function transfer(uint256 actorSeed, uint256 amount) public {

address from = actors[actorSeed % actors.length];

address to = address(uint160(uint256(keccak256(abi.encode(amount)))));

vm.prank(from);

try token.transfer(to, amount) {} catch {}

}

function approve(uint256 actorSeed, uint256 amount) public {

address owner = actors[actorSeed % actors.length];

address spender = address(uint160(uint256(keccak256(abi.encode(amount)))));

vm.prank(owner);

token.approve(spender, amount);

}

function sumOfBalances() public view returns (uint256) {

uint256 sum;

for (uint i = 0; i < actors.length; i++) {

sum += token.balanceOf(actors[i]);

}

return sum;

}

}

5. Fork Testing

Mainnet state के साथ test करें:

contract ForkTest is Test {

IERC20 dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);

address whale = 0x...; // DAI whale address

function setUp() public {

// Fork mainnet at specific block

vm.createSelectFork("mainnet", 18_000_000);

}

function testDAITransfer() public {

uint256 balance = dai.balanceOf(whale);

assertGt(balance, 0);

vm.prank(whale);

dai.transfer(address(this), 1000 ether);

assertEq(dai.balanceOf(address(this)), 1000 ether);

}

function testUniswapSwap() public {

// Test real Uniswap contract

IUniswapV2Router router = IUniswapV2Router(0x...");

// Perform swap

// Verify results

}

}

# .env

MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY

# foundry.toml

[rpc_endpoints]

mainnet = "${MAINNET_RPC_URL}"

6. Gas Profiling

Gas usage optimize करें:

forge test --gas-report

Output:

| Contract | Function      | avg   | median | max    |

|----------|---------------|-------|--------|--------|

| Token | transfer | 51234 | 51234 | 51234 |

| Token | approve | 44556 | 44556 | 44556 |

| Token | transferFrom | 58901 | 58901 | 58901 |

Gas Snapshots:

# Create snapshot

forge snapshot

# Compare with previous

forge snapshot --diff

7. Coverage

Test coverage measure करें:

# Generate coverage report

forge coverage

# Detailed report

forge coverage --report lcov

genhtml lcov.info -o coverage/

open coverage/index.html

8. Advanced Patterns

Testing Access Control:

function testOnlyOwnerCanMint() public {

address attacker = address(0x666);

vm.prank(attacker);

vm.expectRevert("Ownable: caller is not the owner");

token.mint(attacker, 1000);

}

function testOwnerCanMint() public {

address owner = token.owner();

vm.prank(owner);

token.mint(address(0x1), 1000);

assertEq(token.balanceOf(address(0x1)), 1000);

}

Testing Time-Based Logic:

function testVestingSchedule() public {

Vesting vesting = new Vesting();

uint256 start = block.timestamp;

vesting.createVesting(alice, 1000 ether, start, 365 days);

// 6 months later

vm.warp(start + 182 days);

uint256 vested = vesting.vestedAmount(alice);

// ~50% should be vested

assertApproxEqAbs(vested, 500 ether, 1 ether);

}

Testing Reentrancy:

contract Attacker {

Victim victim;

uint256 public callCount;

constructor(Victim _victim) {

victim = _victim;

}

function attack() public {

victim.withdraw();

}

receive() external payable {

callCount++;

if (callCount < 5) {

victim.withdraw(); // Reentrancy attempt

}

}

}

function testReentrancyProtection() public {

Victim victim = new Victim();

Attacker attacker = new Attacker(victim);

vm.deal(address(victim), 10 ether);

victim.deposit{value: 1 ether}();

vm.prank(address(attacker));

vm.expectRevert("ReentrancyGuard: reentrant call");

attacker.attack();

}

Best Practices

  • Test सब कुछ:
  • - Happy paths

    - Edge cases

    - Failure cases

    - Access control

    - Events

  • Fuzz critical functions:
  • - Math operations

    - Transfers

    - Approvals

  • Invariants define करें:
  • - Total supply conservation

    - Balance sums

    - State consistency

  • Fork test real protocols:
  • - Integration tests

    - Upgrades

    - Migrations

  • Coverage track करें:
  • - Target 95%+ coverage

    - Focus on critical paths

  • Gas optimize करें:
  • - Gas reports compare करें

    - Snapshots maintain करें

    निष्कर्ष

    Foundry comprehensive testing framework है जो speed और power combine करता है। Proper testing critical है smart contract security के लिए।

    Key takeaways:

    • Unit tests हर function को cover करें
    • Fuzz tests edge cases find करें
    • Invariant tests critical properties verify करें
    • Fork tests real-world scenarios test करें
    • Gas profiling optimization guide करे

    अगले कदम: Solingo पर testing challenges practice करें!

    ---

    अतिरिक्त Resources:

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें