Claude Agent Skill · by Wshobson

Web3 Testing

This covers the essential testing patterns you'll actually use in production smart contract development. It sets up proper Hardhat and Foundry configurations wi

Install
Terminal · npx
$npx skills add https://github.com/wshobson/agents --skill web3-testing
Works with Paperclip

How Web3 Testing fits into a Paperclip company.

Web3 Testing drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md390 lines
Expand
---name: web3-testingdescription: Test smart contracts comprehensively using Hardhat and Foundry with unit tests, integration tests, and mainnet forking. Use when testing Solidity contracts, setting up blockchain test suites, or validating DeFi protocols.--- # 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.jsrequire("@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 ```javascriptconst { 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: MITpragma 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 ```javascriptdescribe("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 ```javascriptdescribe("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 ```javascriptit("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 ```javascriptconst { 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 reportnpx hardhat coverage # Output shows:# File                | % Stmts | % Branch | % Funcs | % Lines |# -------------------|---------|----------|---------|---------|# contracts/Token.sol |   100   |   90     |   100   |   95    |``` ## Contract Verification ```javascript// Verify on Etherscanawait hre.run("verify:verify", {  address: contractAddress,  constructorArguments: [arg1, arg2],});``` ```bash# Or via CLInpx hardhat verify --network mainnet CONTRACT_ADDRESS "Constructor arg1" "arg2"``` ## CI/CD Integration ```yaml# .github/workflows/test.ymlname: 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```