如何编写Solidity单元测试用例验证withdraw函数及解决余额问题
嘿,我刚好也踩过这个坑!在测试Solidity合约的withdraw函数时,直接用Solidity里的this.balance或者[address].balance确实容易出问题——要么是测试合约的上下文干扰,要么是没考虑gas费用的影响,导致余额计算不准。下面给你几个靠谱的解决办法:
方法1:用测试框架的Provider查询链上真实余额
不管你用Hardhat还是Truffle,框架都提供了直接查询链上地址余额的工具,比Solidity内置的balance更准确。以Hardhat + Mocha为例:
const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("Withdraw测试", function () { let targetContract; let owner; let testAddr; beforeEach(async function () { [owner, testAddr] = await ethers.getSigners(); const ContractFactory = await ethers.getContractFactory("你的合约名"); targetContract = await ContractFactory.deploy(); await targetContract.deployed(); }); it("提现后合约余额应为0,所有者余额正确增加", async function () { // 先给合约转1ETH await owner.sendTransaction({ to: targetContract.address, value: ethers.utils.parseEther("1.0") }); // 记录提现前的余额 const ownerBalanceBefore = await ethers.provider.getBalance(owner.address); const contractBalanceBefore = await ethers.provider.getBalance(targetContract.address); // 执行提现交易并等待上链 const withdrawTx = await targetContract.connect(owner).withdraw(); const withdrawReceipt = await withdrawTx.wait(); // 计算这笔交易消耗的gas费(必须考虑,否则余额对比会出错) const gasCost = withdrawReceipt.gasUsed.mul(withdrawReceipt.effectiveGasPrice); // 验证提现后的余额 const ownerBalanceAfter = await ethers.provider.getBalance(owner.address); const contractBalanceAfter = await ethers.provider.getBalance(targetContract.address); expect(contractBalanceAfter).to.equal(0); // 所有者余额 = 原余额 + 合约提现金额 - gas费 expect(ownerBalanceAfter).to.equal(ownerBalanceBefore.add(contractBalanceBefore).sub(gasCost)); }); });
方法2:在合约内自定义余额查询函数
如果你的测试是用Solidity编写的(比如Truffle的Solidity测试),可以在目标合约里加一个简单的余额查询函数,避免测试合约上下文的干扰:
// 目标合约中添加 function getContractBalance() public view returns (uint256) { return address(this).balance; }
然后在Solidity测试里调用这个函数:
pragma solidity ^0.8.0; import "truffle/Assert.sol"; import "truffle/DeployedAddresses.sol"; import "../contracts/你的合约名.sol"; contract TestWithdraw { 你的合约名 targetContract = 你的合约名(DeployedAddresses.你的合约名()); function testWithdrawFunction() public payable { // 给合约转入测试ETH address(targetContract).transfer(msg.value); uint256 contractBalanceBefore = targetContract.getContractBalance(); uint256 ownerBalanceBefore = msg.sender.balance; // 执行提现 targetContract.withdraw(); // 验证结果 uint256 contractBalanceAfter = targetContract.getContractBalance(); Assert.equal(contractBalanceAfter, 0, "提现后合约余额应为0"); // 这里因为Solidity测试里计算gas费比较麻烦,可做宽松验证 Assert.isTrue(msg.sender.balance > ownerBalanceBefore, "所有者余额应增加"); } }
方法3:结合事件验证增强可靠性
除了查余额,还可以让withdraw函数触发事件,通过监听事件来验证提现金额是否正确,再配合余额查询双重确认:
// 目标合约中添加事件 event Withdrawn(address indexed recipient, uint256 amount); function withdraw() public onlyOwner { uint256 withdrawAmount = address(this).balance; payable(msg.sender).transfer(withdrawAmount); emit Withdrawn(msg.sender, withdrawAmount); }
然后在测试里监听事件:
it("提现时应触发正确的Withdrawn事件", async function () { await owner.sendTransaction({ to: targetContract.address, value: ethers.utils.parseEther("0.5") }); // 监听事件并验证参数 await expect(targetContract.connect(owner).withdraw()) .to.emit(targetContract, "Withdrawn") .withArgs(owner.address, ethers.utils.parseEther("0.5")); // 再确认合约余额清零 const contractBalance = await ethers.provider.getBalance(targetContract.address); expect(contractBalance).to.equal(0); });
总的来说,优先用框架的getBalance()方法查询链上真实余额,结合事件验证可以让测试更健壮,避免各种上下文带来的奇怪问题。
内容的提问来源于stack exchange,提问作者Maximus S
相关产品推荐
相关产品推荐

