$npx -y skills add wshobson/agents --skill web3-testingTest 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.
| 1 | # Web3 Smart Contract Testing |
| 2 | |
| 3 | Master comprehensive testing strategies for smart contracts using Hardhat, Foundry, and advanced testing patterns. |
| 4 | |
| 5 | ## When to Use This Skill |
| 6 | |
| 7 | - Writing unit tests for smart contracts |
| 8 | - Setting up integration test suites |
| 9 | - Performing gas optimization testing |
| 10 | - Fuzzing for edge cases |
| 11 | - Forking mainnet for realistic testing |
| 12 | - Automating test coverage reporting |
| 13 | - Verifying contracts on Etherscan |
| 14 | |
| 15 | ## Hardhat Testing Setup |
| 16 | |
| 17 | ```javascript |
| 18 | // hardhat.config.js |
| 19 | require("@nomicfoundation/hardhat-toolbox"); |
| 20 | require("@nomiclabs/hardhat-etherscan"); |
| 21 | require("hardhat-gas-reporter"); |
| 22 | require("solidity-coverage"); |
| 23 | |
| 24 | module.exports = { |
| 25 | solidity: { |
| 26 | version: "0.8.19", |
| 27 | settings: { |
| 28 | optimizer: { |
| 29 | enabled: true, |
| 30 | runs: 200, |
| 31 | }, |
| 32 | }, |
| 33 | }, |
| 34 | networks: { |
| 35 | hardhat: { |
| 36 | forking: { |
| 37 | url: process.env.MAINNET_RPC_URL, |
| 38 | blockNumber: 15000000, |
| 39 | }, |
| 40 | }, |
| 41 | goerli: { |
| 42 | url: process.env.GOERLI_RPC_URL, |
| 43 | accounts: [process.env.PRIVATE_KEY], |
| 44 | }, |
| 45 | }, |
| 46 | gasReporter: { |
| 47 | enabled: true, |
| 48 | currency: "USD", |
| 49 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, |
| 50 | }, |
| 51 | etherscan: { |
| 52 | apiKey: process.env.ETHERSCAN_API_KEY, |
| 53 | }, |
| 54 | }; |
| 55 | ``` |
| 56 | |
| 57 | ## Unit Testing Patterns |
| 58 | |
| 59 | ```javascript |
| 60 | const { expect } = require("chai"); |
| 61 | const { ethers } = require("hardhat"); |
| 62 | const { |
| 63 | loadFixture, |
| 64 | time, |
| 65 | } = require("@nomicfoundation/hardhat-network-helpers"); |
| 66 | |
| 67 | describe("Token Contract", function () { |
| 68 | // Fixture for test setup |
| 69 | async function deployTokenFixture() { |
| 70 | const [owner, addr1, addr2] = await ethers.getSigners(); |
| 71 | |
| 72 | const Token = await ethers.getContractFactory("Token"); |
| 73 | const token = await Token.deploy(); |
| 74 | |
| 75 | return { token, owner, addr1, addr2 }; |
| 76 | } |
| 77 | |
| 78 | describe("Deployment", function () { |
| 79 | it("Should set the right owner", async function () { |
| 80 | const { token, owner } = await loadFixture(deployTokenFixture); |
| 81 | expect(await token.owner()).to.equal(owner.address); |
| 82 | }); |
| 83 | |
| 84 | it("Should assign total supply to owner", async function () { |
| 85 | const { token, owner } = await loadFixture(deployTokenFixture); |
| 86 | const ownerBalance = await token.balanceOf(owner.address); |
| 87 | expect(await token.totalSupply()).to.equal(ownerBalance); |
| 88 | }); |
| 89 | }); |
| 90 | |
| 91 | describe("Transactions", function () { |
| 92 | it("Should transfer tokens between accounts", async function () { |
| 93 | const { token, owner, addr1 } = await loadFixture(deployTokenFixture); |
| 94 | |
| 95 | await expect(token.transfer(addr1.address, 50)).to.changeTokenBalances( |
| 96 | token, |
| 97 | [owner, addr1], |
| 98 | [-50, 50], |
| 99 | ); |
| 100 | }); |
| 101 | |
| 102 | it("Should fail if sender doesn't have enough tokens", async function () { |
| 103 | const { token, addr1 } = await loadFixture(deployTokenFixture); |
| 104 | const initialBalance = await token.balanceOf(addr1.address); |
| 105 | |
| 106 | await expect( |
| 107 | token.connect(addr1).transfer(owner.address, 1), |
| 108 | ).to.be.revertedWith("Insufficient balance"); |
| 109 | }); |
| 110 | |
| 111 | it("Should emit Transfer event", async function () { |
| 112 | const { token, owner, addr1 } = await loadFixture(deployTokenFixture); |
| 113 | |
| 114 | await expect(token.transfer(addr1.address, 50)) |
| 115 | .to.emit(token, "Transfer") |
| 116 | .withArgs(owner.address, addr1.address, 50); |
| 117 | }); |
| 118 | }); |
| 119 | |
| 120 | describe("Time-based tests", function () { |
| 121 | it("Should handle time-locked operations", async function () { |
| 122 | const { token } = await loadFixture(deployTokenFixture); |
| 123 | |
| 124 | // Increase time by 1 day |
| 125 | await time.increase(86400); |
| 126 | |
| 127 | // Test time-dependent functionality |
| 128 | }); |
| 129 | }); |
| 130 | |
| 131 | describe("Gas optimization", function () { |
| 132 | it("Should use gas efficiently", async function () { |
| 133 | const { token } = await loadFixture(deployTokenFixture); |
| 134 | |
| 135 | const tx = await token.transfer(addr1.address, 100); |
| 136 | const receipt = await tx.wait(); |
| 137 | |
| 138 | expect(receipt.gasUsed).to.be.lessThan(50000); |
| 139 | }); |
| 140 | }); |
| 141 | }); |
| 142 | ``` |
| 143 | |
| 144 | ## Foundry Testing (Forge) |
| 145 | |
| 146 | ```solidity |
| 147 | // SPDX-License-Identifier: MIT |
| 148 | pragma solidity ^0.8.0; |
| 149 | |
| 150 | import "forge-std/Test.sol"; |
| 151 | import "../src/Token.sol"; |
| 152 | |
| 153 | contract TokenTest is Test { |
| 154 | Token token; |
| 155 | address owner = address(1); |
| 156 | address user1 = address(2); |
| 157 | address user2 = address(3); |
| 158 | |
| 159 | function setUp() public { |
| 160 | vm.prank(owner); |
| 161 | token = new Token(); |
| 162 | } |
| 163 | |
| 164 | function testInitialSupply() public { |
| 165 | assertEq(token.totalSupply(), 1000000 * 10**18); |
| 166 | } |
| 167 | |
| 168 | function testTransfer() public { |
| 169 | vm.prank(owner); |
| 170 | token.transfer(user1, 100); |
| 171 | |
| 172 | assertEq(token.balanceOf(user1), 100); |
| 173 | assertEq(token.balanceOf(owner), token.totalSupply() - 100); |
| 174 | } |
| 175 | |
| 176 | function testFailTransferInsufficientBalance() public { |
| 177 | vm.prank(user1); |
| 178 | token.transfer(user2, 100); // Sho |