You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 07:40:34