Source: wshobson/agents Original Plugin: blockchain-web3
Web3 Smart Contract Testing
Master comprehensive testing strategies for smart contracts using Hardhat, Foundry, and advanced testing patterns.
When to Use This Skill
- Writing unit tests for smart contracts
- Setting up integration test suites
- Performing gas optimization testing
- Fuzzing for edge cases
- Forking mainnet for realistic testing
- Automating test coverage reporting
- Verifying contracts on Etherscan
Hardhat Testing Setup
JAVASCRIPT
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");
require("hardhat-gas-reporter");
require("solidity-coverage");
module.exports = {
  solidity: {
    version: "0.8.19",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    hardhat: {
      forking: {
        url: process.env.MAINNET_RPC_URL,
        blockNumber: 15000000
      }
    },
    goerli: {
      url: process.env.GOERLI_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    }
  },
  gasReporter: {
    enabled: true,
    currency: 'USD',
    coinmarketcap: process.env.COINMARKETCAP_API_KEY
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY
  }
};
Unit Testing Patterns
JAVASCRIPT
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture, time } = require("@nomicfoundation/hardhat-network-helpers");
describe("Token Contract", function () {
  // Fixture for test setup
  async function deployTokenFixture() {
    const [owner, addr1, addr2] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("Token");
    const token = await Token.deploy();
    return { token, owner, addr1, addr2 };
  }
  describe("Deployment", function () {
    it("Should set the right owner", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      expect(await token.owner()).to.equal(owner.address);
    });
    it("Should assign total supply to owner", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      const ownerBalance = await token.balanceOf(owner.address);
      expect(await token.totalSupply()).to.equal(ownerBalance);
    });
  });
  describe("Transactions", function () {
    it("Should transfer tokens between accounts", async function () {
      const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
      await expect(token.transfer(addr1.address, 50))
        .to.changeTokenBalances(token, [owner, addr1], [-50, 50]);
    });
    it("Should fail if sender doesn't have enough tokens", async function () {
      const { token, addr1 } = await loadFixture(deployTokenFixture);
      const initialBalance = await token.balanceOf(addr1.address);
      await expect(
        token.connect(addr1).transfer(owner.address, 1)
      ).to.be.revertedWith("Insufficient balance");
    });
    it("Should emit Transfer event", async function () {
      const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
      await expect(token.transfer(addr1.address, 50))
        .to.emit(token, "Transfer")
        .withArgs(owner.address, addr1.address, 50);
    });
  });
  describe("Time-based tests", function () {
    it("Should handle time-locked operations", async function () {
      const { token } = await loadFixture(deployTokenFixture);
      // Increase time by 1 day
      await time.increase(86400);
      // Test time-dependent functionality
    });
  });
  describe("Gas optimization", function () {
    it("Should use gas efficiently", async function () {
      const { token } = await loadFixture(deployTokenFixture);
      const tx = await token.transfer(addr1.address, 100);
      const receipt = await tx.wait();
      expect(receipt.gasUsed).to.be.lessThan(50000);
    });
  });
});
Foundry Testing (Forge)
SOLIDITY
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Token.sol";
contract TokenTest is Test {
    Token token;
    address owner = address(1);
    address user1 = address(2);
    address user2 = address(3);
    function setUp() public {
        vm.prank(owner);
        token = new Token();
    }
    function testInitialSupply() public {
        assertEq(token.totalSupply(), 1000000 * 10**18);
    }
    function testTransfer() public {
        vm.prank(owner);
        token.transfer(user1, 100);
        assertEq(token.balanceOf(user1), 100);
        assertEq(token.balanceOf(owner), token.totalSupply() - 100);
    }
    function testFailTransferInsufficientBalance() public {
        vm.prank(user1);
        token.transfer(user2, 100); // Should fail
    }
    function testCannotTransferToZeroAddress() public {
        vm.prank(owner);
        vm.expectRevert("Invalid recipient");
        token.transfer(address(0), 100);
    }
    // Fuzzing test
    function testFuzzTransfer(uint256 amount) public {
        vm.assume(amount > 0 && amount <= token.totalSupply());
        vm.prank(owner);
        token.transfer(user1, amount);
        assertEq(token.balanceOf(user1), amount);
    }
    // Test with cheatcodes
    function testDealAndPrank() public {
        // Give ETH to address
        vm.deal(user1, 10 ether);
        // Impersonate address
        vm.prank(user1);
        // Test functionality
        assertEq(user1.balance, 10 ether);
    }
    // Mainnet fork test
    function testForkMainnet() public {
        vm.createSelectFork("https://eth-mainnet.alchemyapi.io/v2/...");
        // Interact with mainnet contracts
        address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
        assertEq(IERC20(dai).symbol(), "DAI");
    }
}
Advanced Testing Patterns
Snapshot and Revert
JAVASCRIPT
describe("Complex State Changes", function () {
  let snapshotId;
  beforeEach(async function () {
    snapshotId = await network.provider.send("evm_snapshot");
  });
  afterEach(async function () {
    await network.provider.send("evm_revert", [snapshotId]);
  });
  it("Test 1", async function () {
    // Make state changes
  });
  it("Test 2", async function () {
    // State reverted, clean slate
  });
});
Mainnet Forking
JAVASCRIPT
describe("Mainnet Fork Tests", function () {
  let uniswapRouter, dai, usdc;
  before(async function () {
    await network.provider.request({
      method: "hardhat_reset",
      params: [{
        forking: {
          jsonRpcUrl: process.env.MAINNET_RPC_URL,
          blockNumber: 15000000
        }
      }]
    });
    // Connect to existing mainnet contracts
    uniswapRouter = await ethers.getContractAt(
      "IUniswapV2Router",
      "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
    );
    dai = await ethers.getContractAt(
      "IERC20",
      "0x6B175474E89094C44Da98b954EedeAC495271d0F"
    );
  });
  it("Should swap on Uniswap", async function () {
    // Test with real Uniswap contracts
  });
});
Impersonating Accounts
JAVASCRIPT
it("Should impersonate whale account", async function () {
  const whaleAddress = "0x...";
  await network.provider.request({
    method: "hardhat_impersonateAccount",
    params: [whaleAddress]
  });
  const whale = await ethers.getSigner(whaleAddress);
  // Use whale's tokens
  await dai.connect(whale).transfer(addr1.address, ethers.utils.parseEther("1000"));
});
Gas Optimization Testing
JAVASCRIPT
const { expect } = require("chai");
describe("Gas Optimization", function () {
  it("Compare gas usage between implementations", async function () {
    const Implementation1 = await ethers.getContractFactory("OptimizedContract");
    const Implementation2 = await ethers.getContractFactory("UnoptimizedContract");
    const contract1 = await Implementation1.deploy();
    const contract2 = await Implementation2.deploy();
    const tx1 = await contract1.doSomething();
    const receipt1 = await tx1.wait();
    const tx2 = await contract2.doSomething();
    const receipt2 = await tx2.wait();
    console.log("Optimized gas:", receipt1.gasUsed.toString());
    console.log("Unoptimized gas:", receipt2.gasUsed.toString());
    expect(receipt1.gasUsed).to.be.lessThan(receipt2.gasUsed);
  });
});
Coverage Reporting
BASH
# Generate coverage report
npx hardhat coverage
# Output shows:
# File                | % Stmts | % Branch | % Funcs | % Lines |
# -------------------|---------|----------|---------|---------|
# contracts/Token.sol |   100   |   90     |   100   |   95    |
Contract Verification
JAVASCRIPT
// Verify on Etherscan
await hre.run("verify:verify", {
  address: contractAddress,
  constructorArguments: [arg1, arg2]
});
BASH
# Or via CLI
npx hardhat verify --network mainnet CONTRACT_ADDRESS "Constructor arg1" "arg2"
CI/CD Integration
YAML
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm install
      - run: npx hardhat compile
      - run: npx hardhat test
      - run: npx hardhat coverage
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v2
Resources
- references/hardhat-setup.md: Hardhat configuration guide
- references/foundry-setup.md: Foundry testing framework
- references/test-patterns.md: Testing best practices
- references/mainnet-forking.md: Fork testing strategies
- references/contract-verification.md: Etherscan verification
- assets/hardhat-config.js: Complete Hardhat configuration
- assets/test-suite.js: Comprehensive test examples
- assets/foundry.toml: Foundry configuration
- scripts/test-contract.sh: Automated testing script
Best Practices
- Test Coverage: Aim for >90% coverage
- Edge Cases: Test boundary conditions
- Gas Limits: Verify functions don't hit block gas limit
- Reentrancy: Test for reentrancy vulnerabilities
- Access Control: Test unauthorized access attempts
- Events: Verify event emissions
- Fixtures: Use fixtures to avoid code duplication
- Mainnet Fork: Test with real contracts
- Fuzzing: Use property-based testing
- CI/CD: Automate testing on every commit