跳转到内容

智能合约测试

智能合约测试

本页内容正在整理中,欢迎贡献

概述

智能合约一旦部署便不可更改,且直接管理真实资产,因此全面测试至关重要。本文介绍使用 Hardhat(TypeScript)和 Foundry(Solidity)两种框架编写单元测试、集成测试和模糊测试的最佳实践。

主要内容

测试框架选择

框架测试语言优势
Hardhat + ChaiTypeScript熟悉的 JS 生态,模拟 Web3 环境方便
Foundry(forge test)Solidity运行速度快、内置模糊测试、Gas 快照

Hardhat 测试示例

test/Token.test.ts
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 测试示例

test/Token.t.sol
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() // 断言下一次调用 revert
vm.expectEmit(...) // 断言事件触发
vm.mockCall(...) // Mock 合约调用返回值
vm.snapshot() / vm.revertTo() // 快照与回滚状态

测试覆盖率

Terminal window
# Hardhat
npx hardhat coverage
# 生成 coverage/ 目录,查看 HTML 报告
# Foundry
forge coverage
forge coverage --report lcov
genhtml lcov.info --output-directory coverage/

Gas 快照(Foundry)

Terminal window
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);
// 现在可以访问主网真实合约状态
}
Terminal window
forge test --fork-url $MAINNET_RPC_URL

测试最佳实践

  1. AAA 模式:Arrange(准备)→ Act(执行)→ Assert(断言)
  2. 边界值测试:0、最大值、最大值+1
  3. 测试失败路径:确保 revert 在正确条件下触发
  4. 事件测试:验证所有关键操作都发出事件
  5. 权限测试:确保非授权账户无法调用受保护函数
  6. 模糊测试:用随机输入发现边界 bug

深入阅读