使用 Synpress 进行 Web3 dApp 测试
Web3 中最重要的规则:UI 不是真相来源,链才是。 dApp 可以在交易被回滚、替换或从未离开钱包时显示“成功”。每个改变状态的测试都需要以链上检查结束。
设置
首先检查项目使用什么。以下说明适用于 Synpress v4(基于 Playwright)。v3 基于 Cypress,其 API 不同。
npm i -D @synthetixio/synpress @playwright/test viem
在 package.json 中使用 "type": "module",在 tsconfig.json 中使用 "moduleResolution": "Bundler"(参见 examples/tsconfig.json);Synpress 提供 ES 模块。安装 Synpress 所依赖的 @playwright/test 版本(npm ls @playwright/test),否则会出现两个 Playwright 副本且类型不兼容。
运行本地链以便测试快速、免费且可重复。Foundry 的 Anvil 是常用选择:
anvil --chain-id 31337 --block-time 1 # 默认测试助记词提供 10 个有资金账户
# 或在固定区块分叉真实网络,使得每次运行合约状态相同:
anvil --fork-url $RPC_URL --fork-block-number 21000000
绝不要使用曾持有真实资金的助记词。使用 Anvil 测试助记词或专用的丢弃钱包,保存在环境变量中,绝不提交。
钱包设置与缓存
Synpress 构建一个已配置好 MetaMask 的浏览器配置文件并缓存,这样测试无需每次都重新导入钱包。参见 examples/wallet-setup/basic.setup.ts。
npx synpress # 从 wallet-setup 文件夹中每个 *.setup.ts 构建缓存
当设置文件或 MetaMask 版本更改时重建缓存。在 CI 中扩展需要显示:在 xvfb-run 下运行测试。
需要覆盖的流程
| 流程 | 能发现真实 bug 的用例 |
|---|---|
| 连接 | 首次连接、拒绝连接、断开并重新连接、钱包锁定、选错账户 |
| 网络 | dApp 在错误的链上、用户中途切换链、添加钱包缺少的网络 |
| 签名 (SIWE, permits) | 确认、拒绝、用账户 A 签名后切换为 B 再提交 |
| 交易 | 确认、拒绝、余额不足、Gas 价格飙升、替换或加速交易、合约回滚 |
| 代币授权 | 精确 vs 无限授权、授权后消费、未经授权消费 |
| 待处理状态 | 待处理时刷新页面、关闭标签后返回、连续两笔交易 |
| 显示 | 交易后的余额(UI vs 链)、小数点和舍入、极大与极小金额 |
在钱包中拒绝与确认同样重要。许多 dApp 在拒绝后永远卡在“等待确认”状态。
编写测试
模式(完整文件见 examples/transfer.spec.ts):
- 使用
viem读取链上初始状态(getBalance、readContract)。 - 使用 Playwright 定位器驱动 dApp UI,使用
MetaMask助手操作钱包(connectToDapp、confirmSignature、confirmTransaction、rejectTransaction、switchNetwork)。 - 从 UI、事件或网络响应获取交易哈希,然后调用
waitForTransactionReceipt。 - 对收据状态 和 新的链上状态进行断言。然后断言 UI 与其一致。
将与钱包无关的逻辑(金额验证、格式化、错误文案)放在更快的测试中,使用 Synpress 的以太坊钱包模拟或纯 Playwright 模拟 Provider。仅在扩展本身重要的流程中使用真实 MetaMask。
需要关注的典型 bug
- UI 显示成功,但收据状态为
reverted,或者收据根本不存在。 - UI 中的余额是缓存的,交易后未刷新。
- 金额精度:18 位小数的代币显示为浮点数四舍五入;最大值按钮留下灰尘或超出余额 1 wei。
- 链切换后 dApp 仍调用旧链的合约地址。
- 拒绝签名后按钮保持禁用直到刷新。
- 同一操作可在第一笔交易待处理时重复提交。
- 治理:UI 中投票成功但链上提案状态未变(错误的提案 ID、在截止区块后投票)。
报告 Web3 bug 时需提供
网络和链 ID、合约地址、钱包地址(仅测试钱包)、交易哈希和区块号、收据状态、UI 显示的内容 vs 链上显示的内容、MetaMask 版本。区块浏览器链接(或使用 cast run <txhash> 的 Anvil 追踪)是最佳证据。