3. 使用Foundry进行单元测试

使用Foundry进行单元测试 学习卡片

任务说明

本案例将使用Foundry框架为智能合约编写并运行单元测试。目标合约是一个简单的ERC20代币(含铸造与销毁功能),测试将覆盖代币的初始化、转账、授权与铸造销毁逻辑,并通过Foundry的测试框架验证合约行为是否符合预期。

合约实现

首先创建代币合约 src/MyToken.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyToken {
    string public name = "MyToken";
    string public symbol = "MTK";
    uint8 public decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor(uint256 initialSupply) {
        _mint(msg.sender, initialSupply);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        uint256 allowed = allowance[from][msg.sender];
        require(allowed >= amount, "allowance exceeded");
        if (allowed != type(uint256).max) {
            allowance[from][msg.sender] = allowed - amount;
        }
        _transfer(from, to, amount);
        return true;
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }

    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }

    function _transfer(address from, address to, uint256 amount) internal {
        require(to != address(0), "transfer to zero address");
        uint256 bal = balanceOf[from];
        require(bal >= amount, "insufficient balance");
        balanceOf[from] = bal - amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
    }

    function _mint(address to, uint256 amount) internal {
        require(to != address(0), "mint to zero address");
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) internal {
        uint256 bal = balanceOf[from];
        require(bal >= amount, "burn exceeds balance");
        balanceOf[from] = bal - amount;
        totalSupply -= amount;
        emit Transfer(from, address(0), amount);
    }
}

测试代码

创建测试文件 test/MyToken.t.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";

contract MyTokenTest is Test {
    MyToken public ;
    address public alice = address(0xA11CE);
    address public bob = address(0xB0B);

    function setUp() public {
         = new MyToken(1000 ether);
        vm.prank(address(this));
        .transfer(alice, 500 ether);
    }

    function test_InitialSupply() public view {
        assertEq(.totalSupply(), 1000 ether);
        assertEq(.balanceOf(address(this)), 500 ether);
        assertEq(.balanceOf(alice), 500 ether);
    }

    function test_Transfer() public {
        vm.prank(alice);
        .transfer(bob, 200 ether);
        assertEq(.balanceOf(alice), 300 ether);
        assertEq(.balanceOf(bob), 200 ether);
    }

    function test_TransferInsufficientBalance() public {
        vm.prank(alice);
        vm.expectRevert("insufficient balance");
        .transfer(bob, 501 ether);
    }

    function test_ApproveAndTransferFrom() public {
        vm.prank(alice);
        .approve(address(this), 100 ether);
        .transferFrom(alice, bob, 100 ether);
        assertEq(.balanceOf(bob), 100 ether);
        assertEq(.allowance(alice, address(this)), 0);
    }

    function test_Mint() public {
        .mint(bob, 50 ether);
        assertEq(.balanceOf(bob), 50 ether);
        assertEq(.totalSupply(), 1050 ether);
    }

    function test_Burn() public {
        vm.prank(alice);
        .burn(100 ether);
        assertEq(.balanceOf(alice), 400 ether);
        assertEq(.totalSupply(), 900 ether);
    }

    function test_BurnExceedsBalance() public {
        vm.prank(alice);
        vm.expectRevert("burn exceeds balance");
        .burn(501 ether);
    }
}

SVG:测试执行流程与断言关系

MyToken.sol constructor(1000 ether) transfer / transferFrom approve / allowance mint / burn balanceOf / totalSupply MyToken.t.sol setUp(): 部署+初始转账 test_InitialSupply test_Transfer (+revert) test_ApproveAndTransferFrom test_Mint / test_Burn forge test 编译合约与测试 执行每个 test_* 函数 捕获 revert / 断言 输出通过/失败统计 关键断言与预期 assertEq(totalSupply, 1000 ether) → 初始总量正确 assertEq(balanceOf[alice], 300 ether) → 转账后余额减少 expectRevert("insufficient balance") → 超额转账被拒绝 验证命令 forge build # 编译合约与测试 forge test -vvv # 运行全部测试并显示详细日志

运行与验证

在项目根目录执行以下命令:

forge build
forge test -vvv

预期输出中所有测试均显示 [PASS],例如:

[PASS] test_ApproveAndTransferFrom()
[PASS] test_Burn()
[PASS] test_BurnExceedsBalance()
[PASS] test_InitialSupply()
[PASS] test_Mint()
[PASS] test_Transfer()
[PASS] test_TransferInsufficientBalance()
Suite result: ok. 7 passed; 0 failed; 0 skipped

判定标准:若7个测试全部通过且无失败,则说明代币合约的初始化、转账、授权、铸造与销毁逻辑均符合预期;若任一测试失败,则根据 forge test -vvv 输出的具体断言信息定位并修复合约或测试代码。