name: web3-testing description: 使用Hardhat和Foundry全面测试智能合约,包括单元测试、集成测试和主网分叉。适用于测试Solidity合约、设置区块链测试套件或验证DeFi协议。
Web3智能合约测试
掌握使用Hardhat、Foundry和高级测试模式进行智能合约全面测试的策略。
何时使用此技能
- 为智能合约编写单元测试
- 设置集成测试套件
- 执行气体优化测试
- 对边缘情况进行模糊测试
- 分叉主网进行现实测试
- 自动化测试覆盖率报告
- 在Etherscan上验证合约
Hardhat测试设置
// 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
}
};
单元测试模式
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture, time } = require("@nomicfoundation/hardhat-network-helpers");
describe("Token Contract", function () {
// 测试设置的夹具
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("部署", function () {
it("应设置正确的所有者", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
expect(await token.owner()).to.equal(owner.address);
});
it("应将总供应量分配给所有者", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
});
describe("交易", function () {
it("应在账户间转移代币", async function () {
const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
await expect(token.transfer(addr1.address, 50))
.to.changeTokenBalances(token, [owner, addr1], [-50, 50]);
});
it("如果发送者没有足够的代币应失败", 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("余额不足");
});
it("应发出Transfer事件", 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("基于时间的测试", function () {
it("应处理时间锁定操作", async function () {
const { token } = await loadFixture(deployTokenFixture);
// 增加时间1天
await time.increase(86400);
// 测试时间相关功能
});
});
describe("气体优化", function () {
it("应高效使用气体", 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测试 (Forge)
// 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); // 应失败
}
function testCannotTransferToZeroAddress() public {
vm.prank(owner);
vm.expectRevert("无效的接收者");
token.transfer(address(0), 100);
}
// 模糊测试
function testFuzzTransfer(uint256 amount) public {
vm.assume(amount > 0 && amount <= token.totalSupply());
vm.prank(owner);
token.transfer(user1, amount);
assertEq(token.balanceOf(user1), amount);
}
// 使用作弊码测试
function testDealAndPrank() public {
// 向地址提供ETH
vm.deal(user1, 10 ether);
// 模拟地址
vm.prank(user1);
// 测试功能
assertEq(user1.balance, 10 ether);
}
// 主网分叉测试
function testForkMainnet() public {
vm.createSelectFork("https://eth-mainnet.alchemyapi.io/v2/...");
// 与主网合约交互
address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
assertEq(IERC20(dai).symbol(), "DAI");
}
}
高级测试模式
快照和恢复
describe("复杂状态变化", function () {
let snapshotId;
beforeEach(async function () {
snapshotId = await network.provider.send("evm_snapshot");
});
afterEach(async function () {
await network.provider.send("evm_revert", [snapshotId]);
});
it("测试1", async function () {
// 进行状态变化
});
it("测试2", async function () {
// 状态已恢复,干净状态
});
});
主网分叉
describe("主网分叉测试", 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
}
}]
});
// 连接到现有的主网合约
uniswapRouter = await ethers.getContractAt(
"IUniswapV2Router",
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
);
dai = await ethers.getContractAt(
"IERC20",
"0x6B175474E89094C44Da98b954EedeAC495271d0F"
);
});
it("应在Uniswap上交换", async function () {
// 使用真实的Uniswap合约测试
});
});
模拟账户
it("应模拟鲸鱼账户", async function () {
const whaleAddress = "0x...";
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [whaleAddress]
});
const whale = await ethers.getSigner(whaleAddress);
// 使用鲸鱼的代币
await dai.connect(whale).transfer(addr1.address, ethers.utils.parseEther("1000"));
});
气体优化测试
const { expect } = require("chai");
describe("气体优化", function () {
it("比较不同实现的燃气使用情况", 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("优化燃气:", receipt1.gasUsed.toString());
console.log("未优化燃气:", receipt2.gasUsed.toString());
expect(receipt1.gasUsed).to.be.lessThan(receipt2.gasUsed);
});
});
覆盖率报告
# 生成覆盖率报告
npx hardhat coverage
# 输出显示:
# 文件 | % 语句 | % 分支 | % 函数 | % 行数 |
# -------------------|---------|----------|---------|---------|
# contracts/Token.sol | 100 | 90 | 100 | 95 |
合约验证
// 在Etherscan上验证
await hre.run("verify:verify", {
address: contractAddress,
constructorArguments: [arg1, arg2]
});
# 或通过CLI
npx hardhat verify --network mainnet CONTRACT_ADDRESS "构造函数参数1" "参数2"
CI/CD集成
# .github/workflows/test.yml
name: 测试
on: [push, pull_request]
jobs:
测试:
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: 上传覆盖率到Codecov
uses: codecov/codecov-action@v2
资源
- references/hardhat-setup.md: Hardhat配置指南
- references/foundry-setup.md: Foundry测试框架
- references/test-patterns.md: 测试最佳实践
- references/mainnet-forking.md: 分叉测试策略
- references/contract-verification.md: Etherscan验证
- assets/hardhat-config.js: 完整Hardhat配置
- assets/test-suite.js: 综合测试示例
- assets/foundry.toml: Foundry配置
- scripts/test-contract.sh: 自动化测试脚本
最佳实践
- 测试覆盖率: 目标>90%
- 边缘情况: 测试边界条件
- 气体限制: 验证函数不达到区块气体限制
- 重入: 测试重入漏洞
- 访问控制: 测试未授权访问尝试
- 事件: 验证事件发射
- 夹具: 使用夹具避免代码重复
- 主网分叉: 使用真实合约测试
- 模糊测试: 使用基于属性的测试
- CI/CD: 每次提交自动化测试