网站优化

网站优化

Products

当前位置:首页 > 网站优化 >

如何用Solidity和Foundry高效测试智能合约中的Event?

GG网络技术分享 2026-04-15 20:49 3


为什么测试 Event 让我想哭

说实话,写智能合约测试简直就是一种自虐。你以为你写完了逻辑,你以为你覆盖了所有的边界情况,后来啊呢?前端跑过来跟你说:“嘿,哥们,我监听不到事件啊,你的合约是不是挂了?”那一刻,你的心情绝对是崩溃的。 拖进度。 event 不仅仅是日志,它是链下系统和链上世界沟通的唯一桥梁,是那根救命的稻草。如果这根稻草断了或者这根稻草传递的信息是错的,那整个去中心化应用就变成了瞎子。

所以我们得测试。不仅要测试,还要用 Foundry 这种硬核工具来测试。为什么是 Foundry?主要原因是它快,主要原因是它用 Rust 写的,主要原因是它能让你在测试里像上帝一样控制时间、余额和存储。但是 Foundry 测试 Event 的语法有时候真的让人摸不着头脑,特别是那个 expectEmit那一堆布尔参数,每次写我都得去翻文档,或者干脆瞎蒙,反正运气好就能过。

纸上谈兵·solidity·Foundry 实战》智能合约 Event 测试全攻略

今天我们就来聊聊怎么用 Solidity 和 Foundry 高效测试顺序, 怎么偷懒忽略顺序,还有怎么去解码那些该死的非 indexed 参数。准备好了吗?没有?没有也得看,主要原因是代码不会自己写完,踩雷了。。

先来个简单的:基础 Event 测试

假设我们有一个简单的 Token 合约,就像那种满大街都是的 ERC20 标准克隆体。它有一个 transfer 函数, 当冤大头了。 转账的时候会触发一个 Transfer 事件。这太基础了基础到如果你不测试它,你的代码审查员都会用看垃圾的眼神看你。

代码大概长这样, 别嫌它简单,简单才是真理:

换个角度。 contract Token { event Transfer; function transfer external { emit Transfer; } function batchTransfer external { for { emit Transfer; } }}

算是吧... 看到了吗?那个 indexed 关键字,它很重要。它决定了这个参数是作为日志的 topics 存储还是塞进 data 里面。如果你不搞懂这个, 后面的测试你根本没法做,你会一直看到 log != expected log 这种让人想砸键盘的错误。

在 Foundry 里 测试这个单次转账的事件,你需要用到 expectEmit。这个函数就像是你在对 Foundry 说:“嘿, 听好了接下来我要发生一件事,你给我瞪大眼睛看好了必须跟我描述的一模一样,不然我就报错给你看。”

看这个测试函数, 虽然它看起来很规整,但我写的时候可是纠结了很久:

function testEmitTransfer public { address to = address; uint256 amount = 100; // 告诉 Foundry:我期望捕捉一个 等着瞧。 Transfer 事件 vm.expectEmit; // 写出期望事件 emit Transfer, to, amount); // 调用触发事件的函数 token.transfer;}

注意到了吗?vm.expectEmit。这四个布尔值到底是什么鬼?它们分别是 checkTopic1, checkTopic2, checkTopic3, checkData。主要原因是我们的 Transfer 事件里 fromtoindexed 的,所以它们对应 topic1 和 topic2,我们要检查,所以是 true。没有第三个 indexed 参数,所以第三个是 false。再说说那个 true 是检查 data 字段,也就是 amount。这逻辑简直反人类,但习惯了就好,真的。

批量操作的噩梦:顺序敏感

好了 单次转账太简单了那是给新手练手的。现实世界里为了省 Gas,我们经常要做批量操作。比如 batchTransfer一次性转给好几个人。这时候,事件就会像连珠炮一样发射出来,正宗。。

心情复杂。 如果你还是用 expectEmit那你就要小心了。Foundry 是个强迫症患者,它要求事件的顺序必须严格一致。你先发了 Alice 的事件, 再发 Bob 的,那你测试里写期望的时候,必须先写 Alice,再写 Bob。如果你写反了?嘿嘿,测试直接爆炸。

看看这个“正确”的测试, 多么的枯燥:

function testBatchTransferEmitsMultipleEvents public { address memory recipients = new address; recipients = address; recipients = address; recipients = address; uint256 amount = 100; // 每个事件都 这家伙... 需要单独 expectEmit vm.expectEmit; emit Transfer, recipients, amount); vm.expectEmit; emit Transfer, recipients, amount); vm.expectEmit; emit Transfer, recipients, amount); token.batchTransfer;}

这还没完,如果你手一抖,把顺序写错了比如你先写了 Bob,再写了 Alice, 客观地说... 那你就等着看红色的报错吧。就像下面这个倒霉的测试:

运行的时候, Foundry 会毫不留情地给你甩出一大堆 Trace, 功力不足。 告诉你哪里不对。这报错信息长得像裹脚布一样,看着就让人头晕:

... testBatchTransferWrongOrder Traces: TokenTest::testBatchTransferWrongOrder ├─ VM::expectEmit │ └─ ← ├─ emit Transfer ├─ VM::expectEmit │ └─ ← ├─ emit Transfer ├─ VM::expectEmit │ └─ ← ├─ emit Transfer ├─ Token::batchTransfer │ ├─ emit Transfer │ ├─ emit Transfer │ ├─ emit Transfer │ └─ ← └─ ← log != expected log...,心情复杂。

看到了吗?log != expected log。这简直是废话文学,当然不等于,顺序都乱了。 给力。 这种严格匹配有时候真的很烦,特别是当你不在乎顺序,只在乎“这堆事件是不是都发了”的时候。

偷懒大法:忽略顺序, 手动检查

既然 Foundry 这么死板,那我们能不能灵活一点?当然可以。我们可以用 recordLogs。这个函数就像是把一段时间内发生的所有事件都录下来 存到一个列表里然后我们自己去遍历这个列表, 栓Q! 像在垃圾堆里找宝藏一样找我们需要的事件。

这种方式虽然代码写起来多一点,但是它不关心顺序!你爱先发谁先发谁,反正我再说说都要查一遍。这对于批量操作或者那种异步触发的事件简直是救命稻草,从一个旁观者的角度看...。

来看看怎么写这种“手动挡”的测试:

function testBatchTransferIgnoreOrder public { address memory recipients = new address; recipients = address; recipients = address; recipients = address; uint256 amount = 100; // 开始记录日志 vm.recordLogs; // 施行函数 token.batchTransfer; Vm.Log memory entries = vm.getRecordedLogs; assertEq; // keccak256") bytes32 expectedSig = keccak256"); bool foundAlice; for { assertEq; // indexed 参数 address from = address)); address to = address)); // 非 indexed 参数 uint256 decodedAmount = abi.decode); emit log_named_address; emit log_named_uint; assertEq, "Wrong sender"); assertEq; assertEq; if foundAlice = true; } assertTrue;},不忍直视。

这段代码里充满了各种强制类型转换, 什么 uint160address什么 abi.decode。看着就眼花。但是没办法,EVM 的日志就是这么存储的。topics 里存的是哈希值和 indexed 的参数,data 里存的是原始数据。你想读出来就得自己动手解码。

特别是这个 abi.decode)它是专门用来处理非 indexed 参数的。如果你的事件里没有 indexed 那所有东西都塞在 data 里你就得靠这个把它抠出来。 奥利给! 这感觉就像是在做手术,稍微手抖一下类型不对,测试就挂了。

某些特定的参数是否存在。比如 你只关心 Alice 有没有收到钱,至于 Bob 和 Charlie,你根本懒得管,那你就在循环里只查 Alice 的地址。这就是自由!虽然自由的代价是写更多的代码,搞起来。。

工具对比:谁才是测试界的王者?

说了这么多 Foundry 的好话,我们也不能忘了其他的工具。毕竟选择太多了。为了让你觉得这篇文章更有“技术含量”, 牛逼。 我特意搞了个表格,对比一下几个主流的测试框架。看看它们在 Event 测试方面到底谁更胜一筹,或者谁更让人头大。

特性/工具 Foundry Hardhat Truffle
测试语言 Solidity JavaScript/TypeScript JavaScript
Event 测试方式 expectEmit 严格匹配 或 recordLogs 手动解析 异步监听 emit 事件, 语法像 JS Promise 回调函数监听,老派风格
调试体验 极快,但 Trace 信息有时过于冗长 Console.log 大法,亲切但慢 调试器像上个世纪的产物
学习曲线 陡峭 平缓 平缓
运行速度 光速 龟速 慢速

看这个表格,Foundry 在速度上是碾压的,但是在易用性上,Hardhat 那种 JS 的写法确实更符合前端开发者的直觉。不过 既然你都开始写 Solidity 智能合约了咬咬牙把 Foundry 学会吧,毕竟时间就是金钱,而 Gas 费也是金钱。

Indexed 参数的那些破事

我们再回头聊聊 indexed。这玩意儿虽然好,能让你在链上通过日志过滤器快速查找事件,但是它有限制。一个事件最多只能有 3 个 indexed 参数。 不错。 为什么?主要原因是 EVM 的日志结构里只有 4 个 topics 第一个还得存事件签名的哈希,所以只剩下 3 个坑位给你填。

如果你非要塞 4 个 indexed 进去?编译器会直接给你一个大大的红叉。这时候你就得妥协, 把不那么重要的参数去掉 indexed扔到 data 里去。 我整个人都不好了。 这就像坐公交车,座位有限,没座位的就只能去挤车箱了。

在测试的时候, 如果你把参数设为了 indexed你在 expectEmit 里就得对应地把那个布尔值设为 true。如果你设了 indexed 但在测试里写 false Foundry 会以为你不在乎这个参数,它就不会检查。这听起来很灵活,但其实吧是个坑。万一你手滑写错了明明参数错了但测试通过了那上线了就是灾难。

所以我的建议是:除非你真的不在乎那个参数,否则全部检查!true, true, true, true!让 Foundry 帮你把好每一道关。毕竟多写几个 true 又不会少块肉。

痛苦并快乐着

写到这里我都不知道自己说了些什么。好像是在讲 Foundry,又好像是在发牢骚。不过这就是智能合约开发的日常啊。你永远不知道下一个 Bug 藏在哪里也许是一个简单的拼写错误,也许是对 Event 顺序的误解。

场景了。剩下的 1%,那就是玄学领域了可能需要你祭天才能解决,整起来。。

泰酷辣! 再说说 再送大家一张表格,一下我们在 Foundry 里测试 Event 的几种姿势,免得你们看完文章转头就忘了。

测试场景 推荐方法 代码复杂度 心情指数
单次事件触发 vm.expectEmit 😊
批量事件 多次 vm.expectEmit 😐
批量事件 vm.recordLogs + 循环遍历 😫
只检查特定参数 vm.recordLogs + assertEq 😤

交学费了。 好了 文章写完了我的废话也吐完了。希望你们在测试 Event 的时候能少掉几根头发。如果还是搞不定,那就去喝杯咖啡,或者去 GitHub 上抄……哦不参考一下别人的代码。毕竟站在巨人的肩膀上是进步的阶梯。祝你们的合约永远没有 Revert,Event 永远能被正确捕获!再见!


提交需求或反馈

Demand feedback