Bull's blog Bull's blog
首页
工作
  • 分类
  • 标签
  • 归档
关于

Bull

首页
工作
  • 分类
  • 标签
  • 归档
关于
  • 马上消费

  • 斗虫

  • 天眼查

  • 某米

  • 自学笔记

    • 《10年测试老兵学习区块链:用 Hardhat + Chai 写一个完整可落地的 MiniERC20 测试套件》
      • 运行效果
      • 测试脚本核心功能解析
      • 测试脚本示例
      • 运行测试
    • 《Playwright MCP - 自然语言编写web自动化测试》
  • 工作经历
  • 自学笔记
wangyang
2025-12-02
目录

《10年测试老兵学习区块链:用 Hardhat + Chai 写一个完整可落地的 MiniERC20 测试套件》

# 运行效果

  1. 日志
MiniERC20 基础测试
Gas费检查点 - 合约部署: 266,858 gas ✔ 基础功能_初始余额检查 (382ms)

✅ 余额为0用户转账任意金额应revert 触发 revert
✅ 余额不足:余额1000尝试转1001应revert 触发 revert
✅ 转账到零地址应revert 触发 revert
✅ 转账金额为0应revert 触发 revert
✔ 负向操作测试集合完成

Gas费检查点 - 转账给自身: 26,851 gas
总供应量检查点 - 转账前后总量保持一致: 1000
✔ 转账给自身(边缘正向)完成

开始随机 Fuzz 转账测试
Fuzz #33 input:[amount:33,from:0x70997970C51812dc3A010C7d01b50e0d17dc79C8,to:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266] Gas: 34,451 gas
✅ Fuzz负向:余额不足 触发 revert
.....
✔ 随机 Fuzz 转账测试 (92ms) 完成

Gas费检查点 - owner转300给user1: 51,563 gas
Gas费检查点 - user1转150给user2: 51,539 gas
Gas费检查点 - user2转50给owner: 34,451 gas
✔ 多次转账总供应量不变性测试 完成
  5 passing (500ms)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 覆盖率报告 示例图片

# 本文适用于什么情况or人

本文以自定义的 MiniERC20 智能合约 为测试对象,旨在通过实际操作帮助初学者系统理解区块链测试的核心方法和实践流程。MiniERC20 基于以太坊生态,是最广泛使用的合约类型之一,适合作为理解区块链测试的入门示例。

本测试设计的目标包括:

掌握区块链测试的核心概念与方法,通过实际合约操作加深理解

验证 MiniERC20 合约的基本功能,包括部署、账户余额查询、转账操作及事件触发

学习使用 Hardhat + Chai 框架,构建专业、可重复的智能合约测试脚本

# 本文涉及的知识点

为了完整理解 MiniERC20 合约测试的设计与实践,本文重点涵盖以下知识点:

  1. 区块链测试基础概念
  • 状态机(State Machine):智能合约可以抽象为状态机,所有存储变量(storage slot)组成状态空间,每次函数调用是一次状态转移。

  • 不变性(Invariant):关键约束条件,如总供应量恒定、账户余额不为负,用于验证合约核心逻辑安全。

  • Gas 机制:以太坊交易的燃料消耗原理,以及 Gas 对函数优化和性能评估的影响。

  • EVM 日志系统:事件触发与日志记录机制,如何通过日志验证业务逻辑。

  • 合约状态管理:理解 storage slot、struct、mapping 的存储布局,及 snapshot/rollback 技术对测试的支持。

  1. 高级测试方法
  • 状态空间覆盖(State Space Testing):通过组合函数参数与合约状态,生成全局状态空间进行测试,而不是依赖行为链路。

  • 独立用例设计:每个测试用例独立验证单一行为,避免跨用例依赖污染状态。

  • Fixture 使用:利用 Hardhat loadFixture 创建标准化测试环境,实现测试复用与快速回滚。

  • 随机化测试(Fuzz Testing):通过随机输入和状态组合模拟多样化操作,发现潜在异常或边界条件。

  • Invariant 校验:持续监控合约核心约束,确保在任意状态和操作组合下不变量成立。

  • 性能分析(Gas 检查):记录关键操作 Gas 消耗,为优化合约性能提供依据。

  1. Storage 与 Struct 深入理解
  • Storage slot:合约状态在链上的实际存储单元,决定了如何精确修改、读取合约状态。

  • Struct 与 mapping:逻辑数据结构在链上映射为多个 slot,字段偏移和哈希计算是状态操作的关键。

  • 低层状态操作:通过 getStorageAt 和 setStorageAt 直接操作 slot,可模拟异常状态,支持 fuzz 与 invariant 测试。

# 测试脚本核心功能解析

本测试脚本基于Hardhat+Chai框架实现,涵盖智能合约测试的七大核心功能:

  1. 测试环境复用(Fixtures) 使用loadFixture创建标准化测试环境,确保测试的一致性和执行效率,避免重复部署合约带来的资源浪费。

  2. 功能正确性验证(正向测试) 验证合约正常场景下的功能表现,包括合约部署、账户余额查询、正常转账等核心操作。

  3. 异常行为验证(负向测试) 测试合约在异常条件下的安全性,如余额不足转账、零地址转账、金额为零的转账等场景的错误处理。

  4. 事件精确验证 通过自定义filterLogsByEvent和parseLogs函数解析合约事件,精确验证事件参数的正确性,确保业务逻辑符合预期。

  5. 随机化测试(Fuzz Testing) 对转账功能进行随机输入测试,模拟各种可能的使用场景,有效提高测试覆盖率和发现潜在漏洞的能力。

  6. 不变性验证 持续监控合约的核心不变量(如总供应量),确保在各种操作下合约约束条件始终成立。

  7. 性能优化(Gas检查) 监控关键操作的Gas消耗情况,为合约性能优化提供数据支持,降低用户使用成本。

# 测试脚本示例

下面是一个完整的MiniERC20智能合约测试脚本示例:

展开代码
const { expect } = require("chai");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { filterLogsByEvent, parseLogs, log, safeReverted, getGasUsed, formatGasInfo,randomAmount,randomFromTo } = require("./helper");

describe("MiniERC20 基础测试", function () {

  // 合约部署时候的初始供应量是1000
  const initialSupply = 1000n;


  // --------------------------
  async function deployMiniERC20Fixture() {
    const [owner, user1, user2] = await ethers.getSigners();
    const Factory = await ethers.getContractFactory("MiniERC20");
    // const deployTx = await Factory.deploy();
    // const receipt = await deployTx.waitForDeployment();
    // const token = receipt;
    const token = await Factory.deploy();
    await token.waitForDeployment();

    
    // 获取部署交易的gas信息
    const deployReceipt = await token.deploymentTransaction().wait();

    // const deployReceipt = await deployTx.deploymentTransaction().wait();
    const gasInfo = formatGasInfo(deployReceipt);
    log(`Gas费检查点 - 合约部署: ${gasInfo.formattedGasUsed}`);

    function balance(user) {
      return token.balanceOf(user.address);
    }

    return {
      token,
      owner,
      user1,
      user2,
      balance,
    };
  }

  it("基础功能_初始余额检查", async function () {
    // 检查发起人余额1000,其他用户0
    const { balance, owner, user1 } = await loadFixture(deployMiniERC20Fixture);
    expect(await balance(owner)).to.equal(initialSupply);
    expect(await balance(user1)).to.equal(0n);
  });

  it("负向操作测试集合", async function() {
    // 预期:余额不足用户转账任意金额应revert
    // 预期:余额不足:余额1000尝试转1001应revert
    const { token, owner, user1, user2 } = await loadFixture(deployMiniERC20Fixture);

    // 余额不足
    await safeReverted(token.connect(user1).transfer(user2.address, 1n),
      "余额为0用户转账任意金额应revert", this);
    await safeReverted(token.connect(owner).transfer(user2.address, 1001n),
      "余额不足:余额1000尝试转1001应revert", this);

    // 转账到零地址
    await safeReverted(token.connect(owner).transfer(ethers.ZeroAddress, 1n),
      "转账到零地址应revert", this);

    // 转账金额为0
    await safeReverted(token.connect(owner).transfer(user1.address, 0n),
      "转账金额为0应revert", this);
  });

  it("转账给自身(边缘正向)", async function() {
    // 预期:转账给自身不改变余额,仅触发Transfer事件
    // 预期:转账给自身不改变货币总量
    const { token, owner, user1, user2, balance } = await loadFixture(deployMiniERC20Fixture);

    const transferAmount = 200n;
    const before = await balance(owner);
    // 获取转账前的总供应量
    const totalBalanceBefore = await token.totalBalance();

    const tx = await token.connect(owner).transfer(owner.address, transferAmount);
    const receipt = await tx.wait();

    const gasInfo = formatGasInfo(receipt);
    log(`Gas费检查点 - 转账给自身: ${gasInfo.formattedGasUsed}`);

    const events = await filterLogsByEvent(receipt, token, "Transfer", ["address", "address", "uint256"]);
    const parsedEvents = parseLogs(events, token);

    if (parsedEvents.length > 0) {
        const parsed = parsedEvents[0].parsed;
        expect(parsed.args.from).to.equal(owner.address);
        expect(parsed.args.to).to.equal(owner.address);
        expect(parsed.args.value).to.equal(transferAmount);
    }

    expect(await balance(owner)).to.equal(before);
    // 检查总供应量没有发生改变
    const totalBalanceAfter = await token.totalBalance();
    expect(totalBalanceAfter).to.equal(totalBalanceBefore);
    log(`总供应量检查点 - 转账前后总量保持一致: ${totalBalanceBefore}`);
  });


  it("随机 Fuzz 转账测试", async function() {
    log("\n\n开始随机 Fuzz 转账测试");
    const { token, owner, user1, balance } = await loadFixture(deployMiniERC20Fixture);

    const iterations = 50; // 可根据需求调整
    for (let i = 0; i < iterations; i++) {
      const [from, to] = randomFromTo([owner, user1]);
      const maxAmount = await balance(from);
      const amount = randomAmount(maxAmount + 50n); // +50n 包含超额场景

      if (amount > maxAmount) {
        // 交易前获取总供应量
        const totalBalanceBefore = await token.totalBalance();
        
        await safeReverted(token.connect(from).transfer(to.address, amount),
          "Fuzz负向:余额不足", this);
        
        // 交易后获取总供应量并验证保持不变(即使交易失败)
        const totalBalanceAfter = await token.totalBalance();
        expect(totalBalanceAfter).to.equal(totalBalanceBefore);
      } else {
        // 转账前获取总供应量
        const totalBalanceBefore = await token.totalBalance();
        
        const tx = await token.connect(from).transfer(to.address, amount);
        const receipt = await tx.wait();
        // 可选 Gas/事件检查
        const gasInfo = formatGasInfo(receipt);
        
        // 转账后获取总供应量并验证保持不变
        const totalBalanceAfter = await token.totalBalance();
        expect(totalBalanceAfter).to.equal(totalBalanceBefore);
        
        log(`Fuzz #${i} input:[amount:${amount},from:${from.address},to:${to.address}] Gas: ${gasInfo.formattedGasUsed}`);
      }
    }
    log("随机 Fuzz 转账测试完成\n\n");
  });


  it("总供应量不变性测试", async function() {
    // 预期:转账后总供应量仍为1000
    const { token, owner, user1, user2, balance } = await loadFixture(deployMiniERC20Fixture);

    // 转账1: owner -> user1 300n
    const tx1 = await token.connect(owner).transfer(user1.address, 300n);
    const receipt1 = await tx1.wait();
    const gasInfo1 = formatGasInfo(receipt1);
    log(`Gas费检查点 - owner转300给user1: ${gasInfo1.formattedGasUsed}`);

    // 转账2: user1 -> user2 150n
    const tx2 = await token.connect(user1).transfer(user2.address, 150n);
    const receipt2 = await tx2.wait();
    const gasInfo2 = formatGasInfo(receipt2);
    log(`Gas费检查点 - user1转150给user2: ${gasInfo2.formattedGasUsed}`);

    // 转账3: user2 -> owner 50n
    const tx3 = await token.connect(user2).transfer(owner.address, 50n);
    const receipt3 = await tx3.wait();
    const gasInfo3 = formatGasInfo(receipt3);
    log(`Gas费检查点 - user2转50给owner: ${gasInfo3.formattedGasUsed}`);

    // 直接调用totalBalance()函数获取总供应量
    const totalSupply = await token.totalBalance();
    expect(totalSupply).to.equal(initialSupply);
  });

});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

# 运行测试

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

npx hardhat test
1

或者指定特定的测试文件:

npx hardhat test test/miniERC20.test.js
1
#web3
概述
《Playwright MCP - 自然语言编写web自动化测试》

← 概述 《Playwright MCP - 自然语言编写web自动化测试》→

最近更新
01
《Playwright MCP - 自然语言编写web自动化测试》
12-05
02
代码块折叠示例
03
30.快速实现接口重构测试---deepdiff库使用
09-21
更多文章>
Theme by Vdoing | Copyright © 2018-2025 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式