# 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
- Happy paths
- Edge cases
- Failure cases
- Access control
- Events
- Math operations
- Transfers
- Approvals
- Total supply conservation
- Balance sums
- State consistency
- Integration tests
- Upgrades
- Migrations
- Target 95%+ coverage
- Focus on critical paths
- 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: