引言
智能合约测试是区块链开发中最关键的环节之一。由于合约一旦部署就无法修改,任何bug都可能导致不可逆的损失。Waffle是专为Solidity设计的轻量级测试框架,以其简洁的API和优秀的TypeScript支持,成为许多开发者的首选测试工具。
本文将系统性地介绍Waffle框架的使用方法,从基础配置到高级技巧,帮助你建立完善的合约测试体系。

一、Waffle基础入门
1.1 Waffle简介与特点
Waffle是一个基于 ethers.js 的智能合约测试框架,它的设计理念是简洁、可扩展、强类型。与Hardhat和Foundry相比,Waffle更轻量,学习曲线更平缓,特别适合中小型项目。
bash
# 创建项目
mkdir my-project && cd my-project
npm init -y
# 安装依赖
npm install --save-dev waffle chai ethers @types/chai @types/mocha ts-node typescript
npm install solc @ethereum-waffle
# 初始化TypeScript配置
npx tsc --init
1.2 项目配置
json
// tsconfig.json
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["mocha", "chai"]
},
"include": ["src/**/*"],
"files": ["waffle.json"]
}
json
// waffle.json
{
"compilerVersion": "0.8.24",
"sourceDirectory": "./src",
"outputDirectory": "./build",
"nodeModulesDirectory": "./node_modules",
"flattenOutputDirectory": "./build/flattened"
}
1.3 首个测试文件
solidity
// src/SimpleStorage.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract SimpleStorage {
uint256 private value;
address public owner;
event ValueChanged(uint256 newValue, address changedBy);
constructor() {
owner = msg.sender;
}
function setValue(uint256 _value) external {
value = _value;
emit ValueChanged(_value, msg.sender);
}
function getValue() external view returns (uint256) {
return value;
}
}
typescript
// test/SimpleStorage.test.ts
import { expect, use } from 'chai';
import { waffle, loadFixture } from 'ethereum-waffle';
import { BigNumber } from 'ethers';
import { ethers } from 'ethers';
import SimpleStorageArtifact from '../build/SimpleStorage.json';
import { SimpleStorage } from '../typechain/SimpleStorage';
use(waffle);
describe('SimpleStorage', () => {
// 使用fixture管理测试状态
async function fixture([wallet, otherWallet]: any[]) {
const contract = await waffle.deployContract(wallet, SimpleStorageArtifact);
return { contract, wallet, otherWallet };
}
let contract: SimpleStorage;
let wallet: any;
let otherWallet: any;
beforeEach(async () => {
const fixtures = await loadFixture(fixture);
contract = fixtures.contract;
wallet = fixtures.wallet;
otherWallet = fixtures.otherWallet;
});
describe('setValue', () => {
it('should set value correctly', async () => {
const newValue = 42;
await contract.setValue(newValue);
expect(await contract.getValue()).to.eq(newValue);
});
it('should emit ValueChanged event', async () => {
const newValue = 100;
await expect(contract.setValue(newValue))
.to.emit(contract, 'ValueChanged')
.withArgs(newValue, wallet.address);
});
it('should allow anyone to set value', async () => {
const newValue = 999;
// 从其他钱包调用
await contract.connect(otherWallet).setValue(newValue);
expect(await contract.getValue()).to.eq(newValue);
});
});
describe('getValue', () => {
it('should return initial value of 0', async () => {
expect(await contract.getValue()).to.eq(0);
});
});
describe('owner', () => {
it('should set correct owner', async () => {
expect(await contract.owner()).to.eq(wallet.address);
});
});
});
二、Waffle核心API详解
2.1 合约部署
typescript
// 基础部署
const contract = await waffle.deployContract(wallet, ContractArtifact);
// 带构造函数参数
const contractWithArgs = await waffle.deployContract(
wallet,
ContractWithArgsArtifact,
['arg1', 100] // 构造函数参数
);
// 带自定义选项
const contractWithOptions = await waffle.deployContract(
wallet,
ContractArtifact,
[], // 无构造函数参数
{
value: ethers.utils.parseEther('1'), // 发送ETH
gasLimit: 5000000 // 自定义Gas限制
}
);
// 在指定地址部署(用于升级测试)
const predictedAddress = await wallet.getAddress();
await waffle.deployContract(wallet, ContractArtifact, [], {
salt: ethers.utils.keccak256(ethers.utils.toUtf8Bytes('my-salt'))
});
2.2 Chai断言增强
Waffle扩展了Chai断言库,提供专门针对智能合约的断言:
typescript
import { expect, use } from 'chai';
import { waffle, solidity } from 'ethereum-waffle';
use(solidity); // 启用Solidity特定的断言
describe('Waffle Assertions', () => {
let token: Token;
beforeEach(async () => {
token = await deployToken();
});
// 交易断言
describe('Transaction Assertions', () => {
it('should emit event', async () => {
await expect(token.transfer(recipient, 100))
.to.emit(token, 'Transfer')
.withArgs(wallet.address, recipient, 100);
});
it('should revert with reason', async () => {
await expect(
token.transfer(recipient, 1000000) // 超过余额
).to.be.revertedWith('Insufficient balance');
});
it('should revert without reason', async () => {
await expect(
token.someFunctionThatReverts()
).to.be.reverted;
});
it('should revert with custom error', async () => {
await expect(
token.someFunction()
).to.be.revertedWithCustomError(token, 'Unauthorized');
});
it('should emit value', async () => {
const vault = await deployVault();
await expect(
wallet.sendTransaction({
to: vault.address,
value: ethers.utils.parseEther('1')
})
).to.emit(vault, 'Deposit').withArgs(wallet.address, ethers.utils.parseEther('1'));
});
});
// 状态断言
describe('State Assertions', () => {
it('should change balance correctly', async () => {
const initialBalance = await token.balanceOf(wallet.address);
await token.burn(100);
expect(await token.balanceOf(wallet.address))
.to.eq(initialBalance.sub(100));
});
it('should support big numbers', async () => {
const largeNumber = BigNumber.from('999999999999999999999999999');
await token.mint(wallet.address, largeNumber);
expect(await token.balanceOf(wallet.address))
.to.eq(largeNumber);
});
// 接近相等(用于浮点数处理)
it('should assert approximately equal', async () => {
const expected = ethers.utils.parseEther('1.001');
const actual = ethers.utils.parseEther('1.002');
expect(actual).to.be.closeTo(expected, 1000); // 误差在1000 wei以内
});
});
// 地址断言
describe('Address Assertions', () => {
it('should have correct address', async () => {
expect(await token.name()).to.be.properAddress;
});
it('should emit from correct address', async () => {
await expect(token.transfer(recipient, 100))
.to.emit(token, 'Transfer')
.withArgs(wallet.address, recipient, 100);
});
});
});
2.3 Mock与Stub
typescript
import { mockContract, stubContract } from 'ethereum-waffle';
// 创建Mock合约
describe('Mock Contracts', () => {
it('should create a mock contract', async () => {
const mock = await mockContract(wallet, ExternalContractArtifact);
// 设置Mock行为
mock.getValue.returns(42);
// 在测试中使用
const caller = await deployCaller(mock.address);
expect(await caller.getExternalValue()).to.eq(42);
});
it('should mock with different values', async () => {
const mock = await mockContract(wallet, ExternalContractArtifact);
// 连续调用返回不同值
mock.getValue.onFirstCall().returns(1);
mock.getValue.onSecondCall().returns(2);
mock.getValue.returns(3); // 默认返回
expect(await mock.getValue()).to.eq(1);
expect(await mock.getValue()).to.eq(2);
expect(await mock.getValue()).to.eq(3);
});
it('should verify mock was called', async () => {
const mock = await mockContract(wallet, ExternalContractArtifact);
await mock.getValue();
// 验证调用次数
expect(mock.getValue).to.have.been.calledOnce;
expect(mock.getValue).to.have.been.calledBefore(someOtherCall);
});
});
// 创建Stub合约
describe('Stub Contracts', () => {
it('should create stub with predefined behavior', async () => {
const stub = await stubContract(wallet, ExternalContractArtifact, {
getValue: 42,
getName: 'Test'
});
expect(await stub.getValue()).to.eq(42);
expect(await stub.getName()).to.eq('Test');
});
});
三、Fixture与测试隔离
3.1 Fixture基础
typescript
import { loadFixture } from 'ethereum-waffle';
// 定义可重用的测试fixture
async function tokenFixture([wallet, user]: any[]) {
const token = await waffle.deployContract(wallet, TokenArtifact);
const mintAmount = ethers.utils.parseEther('1000');
await token.mint(wallet.address, mintAmount);
return { token, wallet, user, mintAmount };
}
describe('Token', () => {
let token: Token;
let wallet: any;
let user: any;
let mintAmount: BigNumber;
// 每个测试前重置状态
beforeEach(async () => {
({ token, wallet, user, mintAmount } = await loadFixture(tokenFixture));
});
it('should have correct initial supply', async () => {
expect(await token.totalSupply()).to.eq(mintAmount);
});
});
3.2 复杂Fixture场景
typescript
// 完整的DeFi测试fixture
async function defiFixture([deployer, user1, user2, liquidityProvider]: any[]) {
// 部署Token
const tokenA = await waffle.deployContract(deployer, TokenArtifact, ['TokenA', 'TKA']);
const tokenB = await waffle.deployContract(deployer, TokenArtifact, ['TokenB', 'TKB']);
// 部署AMM合约
const amm = await waffle.deployContract(
deployer,
AMMArtifact,
[tokenA.address, tokenB.address]
);
// 设置初始流动性
const amountA = ethers.utils.parseEther('1000');
const amountB = ethers.utils.parseEther('1000');
await tokenA.mint(liquidityProvider.address, amountA);
await tokenB.mint(liquidityProvider.address, amountB);
await tokenA.connect(liquidityProvider).approve(amm.address, amountA);
await tokenB.connect(liquidityProvider).approve(amm.address, amountB);
await amm.connect(liquidityProvider).addLiquidity(amountA, amountB);
// 给用户分发测试代币
const userAmount = ethers.utils.parseEther('100');
await tokenA.mint(user1.address, userAmount);
await tokenA.mint(user2.address, userAmount);
await tokenA.connect(user1).approve(amm.address, userAmount);
await tokenA.connect(user2).approve(amm.address, userAmount);
return {
tokenA,
tokenB,
amm,
deployer,
user1,
user2,
liquidityProvider
};
}
describe('AMM Integration', () => {
let tokenA: Token;
let tokenB: Token;
let amm: AMM;
let user1: any;
let user2: any;
beforeEach(async () => {
const fixtures = await loadFixture(defiFixture);
({ tokenA, tokenB, amm, user1, user2 } = fixtures);
});
it('should swap tokens correctly', async () => {
const swapAmount = ethers.utils.parseEther('10');
const expectedOutput = await amm.getOutputAmount(tokenA.address, swapAmount);
await expect(() =>
amm.connect(user1).swap(tokenA.address, swapAmount)
).to.changeTokenBalances(
tokenA,
[user1, amm],
[swapAmount.mul(-1), swapAmount]
);
});
});
四、高级测试技巧
4.1 时间操控
typescript
import { mine, time } from 'ethereum-waffle';
describe('Time-dependent Tests', () => {
let staking: Staking;
let user: any;
beforeEach(async () => {
staking = await deployStaking();
user = getUser();
});
it('should release tokens after lock period', async () => {
const stakeAmount = ethers.utils.parseEther('100');
const lockDuration = 7 * 24 * 60 * 60; // 7天
await staking.stake(stakeAmount, lockDuration);
// 快进7天
await time.increase(lockDuration);
await mine(); // 挖出一个区块使时间变化生效
const initialBalance = await ethers.provider.getBalance(user.address);
await staking.withdraw();
const finalBalance = await ethers.provider.getBalance(user.address);
expect(finalBalance.sub(initialBalance)).to.eq(stakeAmount);
});
it('should not allow withdrawal before lock period', async () => {
await staking.stake(ethers.utils.parseEther('100'), 7 * 24 * 60 * 60);
await time.increase(7 * 24 * 60 * 60 - 1); // 差1秒
await expect(staking.withdraw()).to.be.revertedWith('Lock period not ended');
});
it('should handle multiple time jumps', async () => {
const lockDuration = 30 * 24 * 60 * 60;
await staking.stake(ethers.utils.parseEther('100'), lockDuration);
// 分阶段测试
await time.increase(10 * 24 * 60 * 60);
await expect(staking.withdraw()).to.be.revertedWith('Lock period not ended');
await time.increase(20 * 24 * 60 * 60);
await expect(staking.withdraw()).to.be.revertedWith('Lock period not ended');
await time.increase(1 * 24 * 60 * 60); // 总共31天
await mine();
// 现在应该可以提现
await expect(staking.withdraw()).to.not.be.reverted;
});
});
4.2 Gas报告分析
typescript
import { getBalance } from 'ethers';
describe('Gas Optimization', () => {
let contract: GasHeavyContract;
beforeEach(async () => {
contract = await deployGasHeavyContract();
});
it('should track gas usage', async () => {
const tx = await contract.someFunction();
const receipt = await tx.wait();
console.log('Gas used:', receipt.gasUsed.toString());
// 验证Gas使用在预期范围内
expect(receipt.gasUsed).to.be.lt(100000);
});
it('should compare gas between implementations', async () => {
const optimized = await deployOptimizedContract();
const original = await deployOriginalContract();
const optimizedTx = await optimized.process(100);
const optimizedReceipt = await optimizedTx.wait();
const originalTx = await original.process(100);
const originalReceipt = await originalTx.wait();
const savings = originalReceipt.gasUsed
.sub(optimizedReceipt.gasUsed)
.mul(100)
.div(originalReceipt.gasUsed);
console.log(`Gas savings: ${savings.toString()}%`);
expect(savings).to.be.gt(20); // 至少节省20%
});
});
4.3 事件深度测试
typescript
describe('Event Testing', () => {
let token: Token;
beforeEach(async () => {
token = await deployToken();
});
it('should capture all event arguments', async () => {
const tx = await token.mint(user.address, 1000);
const receipt = await tx.wait();
// 从receipt解析事件
const transferEvent = receipt.events?.find(e => e.event === 'Transfer');
expect(transferEvent?.args?.from).to.eq(ethers.constants.AddressZero);
expect(transferEvent?.args?.to).to.eq(user.address);
expect(transferEvent?.args?.value).to.eq(1000);
});
it('should verify event ordering', async () => {
const tx = await token.complexOperation(params);
const receipt = await tx.wait();
const events = receipt.events || [];
expect(events[0].event).to.eq('Approval');
expect(events[1].event).to.eq('Transfer');
expect(events[2].event).to.eq('ComplexOperationComplete');
});
it('should handle anonymous events', async () => {
// 对于匿名事件,需要使用topic过滤
const tx = await token.anonymousEventEmittingFunction();
const receipt = await tx.wait();
// 获取特定topic的事件
const anonymousTopic = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes('AnonymousEvent(uint256)')
);
const logs = receipt.logs?.filter(log =>
log.topics[0] === anonymousTopic
);
expect(logs?.length).to.eq(1);
});
});
4.4 多签名合约测试
typescript
describe('MultiSig Wallet', () => {
const REQUIRED_CONFIRMATIONS = 2;
let multiSig: MultiSig;
let owners: any[];
beforeEach(async () => {
// 部署3个钱包
owners = await ethers.getSigners();
multiSig = await waffle.deployContract(
owners[0],
MultiSigArtifact,
[
owners.map(o => o.address),
REQUIRED_CONFIRMATIONS
]
);
});
it('should require multiple confirmations', async () => {
const destination = recipient.address;
const value = ethers.utils.parseEther('1');
const data = '0x';
// 提交交易
await multiSig.submitTransaction(destination, value, data);
// 第一个owner确认
await multiSig.connect(owners[0]).confirm(0);
// 此时不应该执行(只确认1次,需要2次)
await expect(
multiSig.execute(0)
).to.be.revertedWith('Not enough confirmations');
// 第二个owner确认
await multiSig.connect(owners[1]).confirm(0);
// 现在可以执行
await expect(multiSig.execute(0)).to.not.be.reverted;
});
it('should not allow non-owner to confirm', async () => {
await multiSig.submitTransaction(recipient.address, 0, '0x');
const nonOwner = await generateRandomWallet();
await expect(
multiSig.connect(nonOwner).confirm(0)
).to.be.revertedWith('Not an owner');
});
it('should revoke confirmation', async () => {
await multiSig.submitTransaction(recipient.address, 0, '0x');
await multiSig.connect(owners[0]).confirm(0);
await multiSig.connect(owners[0]).revoke(0);
await expect(multiSig.execute(0))
.to.be.revertedWith('Not enough confirmations');
});
});
五、完整测试示例
5.1 ERC20代币完整测试套件
solidity
// src/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
uint256 public constant MAX_SUPPLY = 1000000 * 10**18;
uint256 public mintingDeadline;
address public minter;
mapping(address => bool) public minters;
event MinterAdded(address indexed account);
event MinterRemoved(address indexed account);
constructor(
string memory name,
string memory symbol,
uint256 _mintingDeadline
) ERC20(name, symbol) {
minter = msg.sender;
mintingDeadline = _mintingDeadline;
}
modifier onlyMinter() {
require(minters[msg.sender] || msg.sender == minter, "Not authorized");
_;
}
function mint(address to, uint256 amount) external onlyMinter {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
function addMinter(address account) external {
require(msg.sender == minter, "Not authorized");
minters[account] = true;
emit MinterAdded(account);
}
function removeMinter(address account) external {
require(msg.sender == minter, "Not authorized");
minters[account] = false;
emit MinterRemoved(account);
}
function transferAfterDeadline(address to, uint256 amount) external {
require(block.timestamp > mintingDeadline, "Minting still active");
_transfer(msg.sender, to, amount);
}
}
typescript
// test/MyToken.test.ts
import { expect, use } from 'chai';
import { waffle, loadFixture } from 'ethereum-waffle';
import { ethers } from 'ethers';
import { MyToken } from '../typechain/MyToken';
import MyTokenArtifact from '../build/MyToken.json';
use(waffle);
describe('MyToken', () => {
const MINTING_DEADLINE = 1700000000;
async function tokenFixture([wallet, user, otherUser]: any[]) {
const token = await waffle.deployContract(
wallet,
MyTokenArtifact,
['MyToken', 'MTK', MINTING_DEADLINE]
);
return { token, wallet, user, otherUser };
}
let token: MyToken;
let wallet: any;
let user: any;
let otherUser: any;
beforeEach(async () => {
({ token, wallet, user, otherUser } = await loadFixture(tokenFixture));
});
describe('Deployment', () => {
it('should set correct name and symbol', async () => {
expect(await token.name()).to.eq('MyToken');
expect(await token.symbol()).to.eq('MTK');
});
it('should set deployer as minter', async () => {
expect(await token.minter()).to.eq(wallet.address);
});
it('should set correct minting deadline', async () => {
expect(await token.mintingDeadline()).to.eq(MINTING_DEADLINE);
});
it('should have zero total supply', async () => {
expect(await token.totalSupply()).to.eq(0);
});
});
describe('Minting', () => {
it('should allow minter to mint', async () => {
const amount = ethers.utils.parseEther('100');
await expect(() =>
token.connect(wallet).mint(user.address, amount)
).to.changeTokenBalance(token, user, amount);
expect(await token.totalSupply()).to.eq(amount);
});
it('should not allow minting beyond max supply', async () => {
const amount = ethers.utils.parseEther('1000001'); // 超过MAX_SUPPLY
await expect(
token.connect(wallet).mint(user.address, amount)
).to.be.revertedWith('Exceeds max supply');
});
it('should not allow non-minter to mint', async () => {
await expect(
token.connect(user).mint(otherUser.address, 100)
).to.be.revertedWith('Not authorized');
});
it('should allow added minter to mint', async () => {
await token.connect(wallet).addMinter(user.address);
expect(await token.minters(user.address)).to.be.true;
const amount = ethers.utils.parseEther('50');
await token.connect(user).mint(otherUser.address, amount);
expect(await token.balanceOf(otherUser.address)).to.eq(amount);
});
});
describe('Burning', () => {
beforeEach(async () => {
await token.connect(wallet).mint(wallet.address, ethers.utils.parseEther('1000'));
});
it('should allow holder to burn', async () => {
const amount = ethers.utils.parseEther('100');
await expect(() =>
token.burn(amount)
).to.changeTokenBalance(token, wallet, amount.mul(-1));
expect(await token.totalSupply()).to.eq(ethers.utils.parseEther('900'));
});
it('should not allow burning more than balance', async () => {
await expect(
token.burn(ethers.utils.parseEther('1001'))
).to.be.revertedWith('ERC20: burn amount exceeds balance');
});
});
describe('Transfers', () => {
beforeEach(async () => {
await token.connect(wallet).mint(wallet.address, ethers.utils.parseEther('1000'));
});
it('should transfer tokens', async () => {
const amount = ethers.utils.parseEther('100');
await expect(() =>
token.transfer(user.address, amount)
).to.changeTokenBalances(
token,
[wallet, user],
[amount.mul(-1), amount]
);
});
it('should not transfer more than balance', async () => {
await expect(
token.transfer(user.address, ethers.utils.parseEther('1001'))
).to.be.revertedWith('ERC20: transfer amount exceeds balance');
});
it('should emit Transfer event', async () => {
const amount = ethers.utils.parseEther('100');
await expect(token.transfer(user.address, amount))
.to.emit(token, 'Transfer')
.withArgs(wallet.address, user.address, amount);
});
});
describe('TransferAfterDeadline', () => {
beforeEach(async () => {
await token.connect(wallet).mint(wallet.address, ethers.utils.parseEther('1000'));
});
it('should allow transfer after deadline', async () => {
// 快进到截止日期之后
await ethers.provider.send('evm_increaseTime', [MINTING_DEADLINE + 1]);
await ethers.provider.send('evm_mine', []);
const amount = ethers.utils.parseEther('100');
await token.transferAfterDeadline(user.address, amount);
expect(await token.balanceOf(user.address)).to.eq(amount);
});
it('should not allow transfer before deadline', async () => {
await expect(
token.transferAfterDeadline(user.address, 100)
).to.be.revertedWith('Minting still active');
});
});
});
5.2 集成测试示例
typescript
// test/StakingIntegration.test.ts
describe('Staking Integration', () => {
async function stakingFixture([owner, staker, rewardRecipient]: any[]) {
// 部署测试代币
const stakingToken = await waffle.deployContract(
owner,
TestTokenArtifact,
['Staking Token', 'STK']
);
// 部署奖励代币
const rewardToken = await waffle.deployContract(
owner,
TestTokenArtifact,
['Reward Token', 'RWD']
);
// 部署质押合约
const staking = await waffle.deployContract(
owner,
StakingArtifact,
[
stakingToken.address,
rewardToken.address,
rewardRecipient.address,
ethers.utils.parseEther('1'), // 1 token per second reward rate
0
]
);
// 分发质押代币给staker
const stakerAmount = ethers.utils.parseEther('1000');
await stakingToken.mint(staker.address, stakerAmount);
await stakingToken.connect(staker).approve(staking.address, stakerAmount);
// 分发奖励代币给staking合约
const rewardAmount = ethers.utils.parseEther('10000');
await rewardToken.mint(owner.address, rewardAmount);
await rewardToken.connect(owner).transfer(staking.address, rewardAmount);
return { stakingToken, rewardToken, staking, owner, staker, rewardRecipient };
}
let staking: Staking;
let stakingToken: TestToken;
let rewardToken: TestToken;
let staker: any;
let owner: any;
beforeEach(async () => {
const fixtures = await loadFixture(stakingFixture);
({ stakingToken, rewardToken, staking, owner, staker } = fixtures);
});
describe('Staking', () => {
it('should accept stakes', async () => {
const stakeAmount = ethers.utils.parseEther('100');
await expect(() =>
staking.connect(staker).stake(stakeAmount)
).to.changeTokenBalance(stakingToken, staker, stakeAmount.mul(-1));
expect(await staking.balanceOf(staker.address)).to.eq(stakeAmount);
});
it('should track time-weighted stakes', async () => {
const stakeAmount = ethers.utils.parseEther('100');
await staking.connect(staker).stake(stakeAmount);
// 快进1小时
await time.increase(3600);
await mine();
// 验证奖励计算
const earned = await staking.earned(staker.address);
expect(earned).to.eq(ethers.utils.parseEther('3600')); // 1 token/s * 3600s
});
it('should allow multiple stakes', async () => {
await staking.connect(staker).stake(ethers.utils.parseEther('50'));
await time.increase(100);
await mine();
await staking.connect(staker).stake(ethers.utils.parseEther('50'));
expect(await staking.balanceOf(staker.address))
.to.eq(ethers.utils.parseEther('100'));
});
});
describe('Rewards', () => {
beforeEach(async () => {
await staking.connect(staker).stake(ethers.utils.parseEther('100'));
});
it('should calculate correct rewards', async () => {
await time.increase(3600);
await mine();
const earned = await staking.earned(staker.address);
expect(earned).to.eq(ethers.utils.parseEther('3600'));
});
it('should allow claim rewards', async () => {
await time.increase(3600);
await mine();
const initialBalance = await rewardToken.balanceOf(staker.address);
await staking.connect(staker).getReward();
const finalBalance = await rewardToken.balanceOf(staker.address);
expect(finalBalance.sub(initialBalance)).to.eq(ethers.utils.parseEther('3600'));
});
it('should compound on restake', async () => {
await time.increase(3600);
await mine();
const earned = await staking.earned(staker.address);
await staking.connect(staker).claimReward();
// 继续质押
await time.increase(3600);
await mine();
const newEarned = await staking.earned(staker.address);
expect(newEarned).to.eq(ethers.utils.parseEther('3600')); // 重新计时
});
});
describe('Withdrawal', () => {
it('should return staked tokens', async () => {
const stakeAmount = ethers.utils.parseEther('100');
await staking.connect(staker).stake(stakeAmount);
await time.increase(100);
await mine();
await expect(() =>
staking.connect(staker).withdraw(stakeAmount)
).to.changeTokenBalance(stakingToken, staker, stakeAmount);
expect(await staking.balanceOf(staker.address)).to.eq(0);
});
it('should include pending rewards in withdrawal', async () => {
const stakeAmount = ethers.utils.parseEther('100');
await staking.connect(staker).stake(stakeAmount);
await time.increase(3600);
await mine();
const earned = await staking.earned(staker.address);
await staking.connect(staker).withdraw(stakeAmount);
// 应该同时收到本金和奖励
expect(await rewardToken.balanceOf(staker.address))
.to.eq(ethers.utils.parseEther('3600'));
});
});
});
总结
Waffle作为Solidity生态中的轻量级测试框架,提供了简洁而强大的测试能力:
| 功能 | Waffle特性 | 优势 |
|---|---|---|
| 部署 | deployContract | 简洁的合约部署API |
| 断言 | Chai扩展 | 专为Solidity优化的断言 |
| Fixture | loadFixture | 高效的状态管理 |
| Mock | mockContract | 便捷的外部依赖模拟 |
| 时间 | time, mine | 精确的时间操控 |
| 类型 | TypeScript原生 | 完整的类型安全 |
核心最佳实践:
- 每个测试独立 – 使用Fixture确保测试间无依赖
- 全面覆盖 – 覆盖正常路径、边界条件、错误处理
- 事件验证 – 不仅验证状态变化,还要验证事件
- Gas监控 – 关注Gas使用,及时发现退化
- 集成测试 – 验证合约间的交互正确性
掌握Waffle的使用,将帮助你构建更加可靠、安全的智能合约。
相关推荐:

发表回复