智能合约测试
智能合约测试
本页内容正在整理中,欢迎贡献。
概述
智能合约一旦部署便不可更改,且直接管理真实资产,因此全面测试至关重要。本文介绍使用 Hardhat(TypeScript)和 Foundry(Solidity)两种框架编写单元测试、集成测试和模糊测试的最佳实践。
主要内容
测试框架选择
| 框架 | 测试语言 | 优势 |
|---|---|---|
| Hardhat + Chai | TypeScript | 熟悉的 JS 生态,模拟 Web3 环境方便 |
| Foundry(forge test) | Solidity | 运行速度快、内置模糊测试、Gas 快照 |
Hardhat 测试示例
import { expect } from "chai";import { ethers } from "hardhat";import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
describe("MyToken", function () { // 使用 fixture 复用部署状态 async function deployTokenFixture() { const [owner, alice, bob] = await ethers.getSigners(); const MyToken = await ethers.getContractFactory("MyToken"); const token = await MyToken.deploy(1_000_000n * 10n ** 18n); return { token, owner, alice, bob }; }
describe("部署", function () { it("应设置正确的初始供应量", async function () { const { token, owner } = await loadFixture(deployTokenFixture); const supply = await token.totalSupply(); expect(await token.balanceOf(owner.address)).to.equal(supply); }); });
describe("转账", function () { it("应正确转移代币", async function () { const { token, owner, alice } = await loadFixture(deployTokenFixture); const amount = ethers.parseEther("100");
await expect(token.transfer(alice.address, amount)) .to.emit(token, "Transfer") .withArgs(owner.address, alice.address, amount);
expect(await token.balanceOf(alice.address)).to.equal(amount); });
it("余额不足时应 revert", async function () { const { token, alice, bob } = await loadFixture(deployTokenFixture); await expect( token.connect(alice).transfer(bob.address, 1n) ).to.be.revertedWithCustomError(token, "ERC20InsufficientBalance"); }); });});Foundry 测试示例
pragma solidity ^0.8.20;import "forge-std/Test.sol";import "../src/MyToken.sol";
contract MyTokenTest is Test { MyToken token; address alice = makeAddr("alice"); address bob = makeAddr("bob");
function setUp() public { token = new MyToken(1_000_000 ether); }
function test_InitialSupply() public view { assertEq(token.balanceOf(address(this)), 1_000_000 ether); }
function test_Transfer() public { token.transfer(alice, 100 ether); assertEq(token.balanceOf(alice), 100 ether); }
function test_RevertWhen_InsufficientBalance() public { vm.expectRevert(); vm.prank(alice); // 模拟 alice 发送交易 token.transfer(bob, 1 ether); }
// 模糊测试:amount 会随机生成 function testFuzz_Transfer(uint256 amount) public { amount = bound(amount, 0, token.totalSupply()); token.transfer(alice, amount); assertEq(token.balanceOf(alice), amount); }}常用 Foundry Cheatcodes
vm.prank(address) // 下一次调用模拟为指定地址vm.startPrank(address) // 开始模拟(持续)vm.stopPrank() // 停止模拟vm.deal(address, uint) // 设置账户 ETH 余额vm.warp(uint256) // 跳转到指定时间戳vm.roll(uint256) // 跳转到指定区块号vm.expectRevert() // 断言下一次调用 revertvm.expectEmit(...) // 断言事件触发vm.mockCall(...) // Mock 合约调用返回值vm.snapshot() / vm.revertTo() // 快照与回滚状态测试覆盖率
# Hardhatnpx hardhat coverage# 生成 coverage/ 目录,查看 HTML 报告
# Foundryforge coverageforge coverage --report lcovgenhtml lcov.info --output-directory coverage/Gas 快照(Foundry)
forge snapshot # 生成 .gas-snapshot 文件forge snapshot --diff # 对比 Gas 变化分叉测试(Fork Testing)
测试合约与链上真实协议(如 Uniswap)的交互:
// 使用主网分叉测试function setUp() public { vm.createSelectFork("https://eth-mainnet.g.alchemy.com/v2/KEY", 19_000_000); // 现在可以访问主网真实合约状态}forge test --fork-url $MAINNET_RPC_URL测试最佳实践
- AAA 模式:Arrange(准备)→ Act(执行)→ Assert(断言)
- 边界值测试:0、最大值、最大值+1
- 测试失败路径:确保
revert在正确条件下触发 - 事件测试:验证所有关键操作都发出事件
- 权限测试:确保非授权账户无法调用受保护函数
- 模糊测试:用随机输入发现边界 bug