使链上应用测试可靠且直接
介绍
测试去中心化应用一直与测试传统的 web 应用有根本的不同。传统的应用处理的是直接的 HTTP 请求和可预测的数据库状态,而链上应用必须驾驭区块链交互的复杂世界:钱包连接、交易批准、网络切换和链状态。每一个交互都带来了现有测试框架未被设计用于处理的独特挑战。
在为 Verified Pools 项目 编写端到端测试时,我们遇到了许多这样的挑战。这个复杂的 DeFi 应用将机构级的流动性基础设施带到了链上市场,需要对前端、后端和智能合约集成之间的复杂用户流程进行测试。例如,用户连接他们的钱包,批准 token 支出,签署 Permit2 消息,向池提供流动性,并在多个合约中管理他们的头寸。原本应该是一个简单的测试工作,却迅速变成了一个长达数周的技术挑战和生产力瓶颈的历程。
E2E 链上测试的核心挑战
测试去中心化应用引入了 web 自动化和区块链交互交叉处的复杂性。标准的端到端测试框架并非天生为此环境而构建,这迫使开发者在编写有效的测试之前解决几个根本问题。
根据我们的经验,有 4 个主要的测试障碍:
1. 钱包扩展设置 每一个测试都需要从头开始安装、配置和提供资金的浏览器钱包。这个过程对每个钱包(如 MetaMask 或 Coinbase Wallet)都是唯一的,并可能导致脆弱的自定义脚本。
2. 不可预测的钱包弹窗 在测试过程中,用户操作会触发钱包弹窗,用于交易批准、消息签名和网络切换。这些弹窗以不一致的时间和 UI 模式异步出现,导致不可预测性和不稳定性。
3. 共享链上状态 在共享区块链上,真正的测试并行是不可能的。当多个测试同时运行时,它们使用相同的钱包地址,并与同一区块链上的相同智能合约进行交互。这导致测试相互干扰,导致难以调试的不可预测的故障。
4. 合约部署和状态管理 在 Solidity 中的智能合约开发(使用 Foundry)和 TypeScript 中的端到端测试之间存在着一个根本的工具缺口。测试需要以特定的初始状态部署合约,但合约地址是不确定的,这破坏了与前端的连接。
OnchainTestKit 如何解决每个问题
OnchainTestKit 直接针对这四个关键问题,提供了专门构建的解决方案:
解决方案 1:自动钱包管理
OnchainTestKit 不是手动设置每种钱包类型,而是自动处理一切,提供了一个适用于所有钱包类型的统一接口。以下是如何使用 Coinbase Wallet 配置测试的示例:
// 使用本地节点和 coinbase 钱包配置测试 const coinbaseWalletConfig = configure() .withLocalNode({ chainId: baseSepolia.id, forkUrl: process.env.E2E_TEST_FORK_URL, forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? "0"), hardfork: "cancun", }) .withCoinbase() .withSeedPhrase({ seedPhrase: DEFAULT_SEED_PHRASE ?? "", password: DEFAULT_PASSWORD, }) .withNetwork({ name: "Base Sepolia", chainId: baseSepolia.id, symbol: "ETH", rpcUrl: "http://localhost:8545", }) .build();结果:对开发者友好,与钱包无关,且易于使用。我们将钱包设置和管理的所有复杂性都隐藏在开发者之外。
解决方案 2:可靠的钱包弹窗处理
OnchainTestKit 提供了一个统一的接口,抽象了所有弹窗的复杂性:
// 无需处理时间和弹窗检测: await coinbaseWallet.handleAction(BaseActionType.CONNECT_TO_DAPP); await coinbaseWallet.handleAction(BaseActionType.HANDLE_TRANSACTION, { approvalType: ActionApprovalType.APPROVE }); await coinbaseWallet.handleAction(BaseActionType.CHANGE_SPENDING_CAP, { approvalType: ActionApprovalType.APPROVE });智能等待策略和重试:OnchainTestKit 使用智能等待策略,理解区块链的时间模式和钱包行为。内置的重试机制自动处理瞬时故障,而自适应超时则根据网络状况和弹窗复杂性进行调整。
结果:测试可靠地通过。不再有因时间问题或不可预测的钱包行为导致的不稳定测试。
解决方案 3:真正的测试隔离
OnchainTestKit 为每个测试提供其自己隔离的 Anvil 区块链节点,从而消除了所有状态冲突:
// 每个测试都获得其自己的 LocalNodeManager test('parallel test A', async ({ localNodeManager, smartContractManager }) => { // 自动在可用端口上启动 Anvil 节点(例如,10543) // 此测试的交易仅影响其自己的区块链 await page.click('#swap-button'); // 测试逻辑... }); test('parallel test B', async ({ localNodeManager, smartContractManager }) => { // 自动在不同的端口上启动单独的 Anvil 节点(例如,10847) // 完全独立的区块链状态 await page.click('#approve-button'); // 测试逻辑... });LocalNodeManager 的工作方式:OnchainTestKit 自动在进程之间分配可用的端口,并为每个测试启动隔离的 Anvil 节点。它支持三种不同的并行测试策略:
1. Fork 现有网络:在特定的区块高度 fork 一个测试网或主网,而无需部署合约。非常适合针对现有协议部署进行测试:
.withLocalNode({ forkUrl: process.env.BASE_MAINNET_RPC_URL, forkBlockNumber: process.env.E2E_TEST_FORK_BLOCK_NUMBER, chainId: 8453 })2. 清理本地状态:从一个全新的区块链开始,并部署所有依赖的合约。非常适合测试新的协议或复杂的状态设置:
.withLocalNode({ chainId: 84532, // 无 fork - 从干净状态开始 }) // 然后通过 smartContractManager 部署合约3. 混合方法:Fork 一个网络并在其上部署额外的测试合约。将真实的协议状态与自定义的测试合约相结合:
.withLocalNode({ forkUrl: process.env.BASE_SEPOLIA_RPC_URL, forkBlockNumber: 10_000_000n, chainId: 84532 }) // 然后根据需要部署额外的测试合约每种方法都提供了完全的测试隔离,具有独立的区块链状态,允许测试操纵时间、账户余额和合约状态,而不会影响其他测试。
智能 RPC 路由:关键的创新之一是自动请求拦截。你的前端可以始终使用固定的 RPC URL,例如 localhost:8545,而 LocalNodeManager 会自动将这些请求路由到每个测试的正确的 Anvil 节点。无需动态配置不同的端口——框架会透明地处理路由。
自动清理:LocalNodeManager 处理完整的节点生命周期,在每个测试完成后优雅地终止每个 Anvil 进程。这确保了没有资源泄漏或端口冲突,即使运行包含数十个并行节点的大型测试套件也是如此。
结果:完全并行化的测试,没有协调开销。CI 时间从数小时缩短到数分钟。
解决方案 4:确定性的合约部署
OnchainTestKit 使用 CREATE2 弥合了 Solidity/TypeScript 的差距,以实现确定性的合约部署:
await smartContractManager.setContractState({ deployments: [\ {\ name: 'MockUSDC',\ salt: '0x1234...', // 用于确定性地址的 CREATE2 salt\ deployer: admin,\ args: ['USD Coin', 'USDC', 6]\ },\ {\ name: 'DEXContract',\ salt: '0x5678...',\ deployer: admin,\ args: [mockUsdcAddress]\ }\ ], calls: [\ { target: mockUsdcAddress, functionName: 'mint', args: [user, amount], account: admin },\ { target: mockUsdcAddress, functionName: 'approve', args: [dexAddress, amount], account: user }\ ] });CREATE2 的工作方式:OnchainTestKit 使用带有固定 salts 的 CREATE2 部署来确保合约始终部署到相同的地址。SmartContractManager 在部署之前预测部署地址,检查这些地址上是否已存在合约,并自动从你的 out/ artifact 目录加载 Foundry artifacts。这在你的 Solidity 合约和 TypeScript 测试之间创建了一个无缝的桥梁。
Foundry 集成:自动加载编译后的合约 artifacts,消除了合约和测试团队之间手动 ABI 管理或部署脚本协调的需要。
结果:可靠的合约测试,具有可预测的地址。从智能合约交互到 UI 反馈的完整用户旅程在所有测试运行中都保持一致。
我们如何在 Verified Pools 中使用 OnchainTestKit
以下是我们 Verified Pools 项目中的一个真实示例,展示了 OnchainTestKit 如何将复杂的链上应用测试转化为简洁、可读的测试:
// walletConfig/metamaskWalletConfig.ts import { baseSepolia } from 'viem/chains'; import { configure } from '@coinbase/onchaintestkit'; export const DEFAULT_PASSWORD = 'PASSWORD'; export const DEFAULT_SEED_PHRASE = process.env.E2E_TEST_SEED_PHRASE; // 用于具有 Base Sepolia fork 的 MetaMask 测试的可重用配置 const metamaskConfig = configure() .withLocalNode({ chainId: baseSepolia.id, forkUrl: process.env.E2E_TEST_FORK_URL, forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? '0'), hardfork: 'cancun', }) .withMetaMask() .withSeedPhrase({ seedPhrase: DEFAULT_SEED_PHRASE ?? '', password: DEFAULT_PASSWORD, }) .withNetwork({ name: 'Base Sepolia', chainId: baseSepolia.id, symbol: 'ETH', rpcUrl: 'http://localhost:8545', // 固定 URL,自动路由到正确的端口 }) .build(); export { metamaskConfig };此测试涵盖了 Verified Pools 中的完整用户旅程:
- 将 MetaMask 钱包连接到链上应用
- 导航到 swap 界面
- 输入 swap 金额 (0.0001 ETH)
- 执行 swap 交易
- 处理所有必需的钱包弹窗(支出上限批准、Permit2 签名、交易确认)
- 验证 swap 是否成功完成
// swap.spec.ts import { createOnchainTest } from '@coinbase/onchaintestkit'; import { NotificationPageType } from '@coinbase/onchaintestkit/wallets/MetaMask'; import { ActionApprovalType, BaseActionType } from '@coinbase/onchaintestkit/wallets/BaseWallet'; import { metamaskConfig } from './walletConfig/metamaskWalletConfig'; const test = createOnchainTest(metamaskConfig); const { expect } = test; test.describe('Verified Pools Swap', () => { test('connect wallet and swap @tx', async ({ page, metamask }) => { if (!metamask) throw new Error('MetaMask fixture is required'); // 导航到 swap 界面 await page.goto('/swap'); // 连接钱包 - OnchainTestKit 处理所有复杂性 await page.getByTestId('ockConnectButton').first().click(); await page.getByTestId('ockModalOverlay') .first() .getByRole('button', { name: 'MetaMask' }) .click(); await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); await page.getByRole('button', { name: /^Accept$/ }).click(); // 设置 swap const inputField = page.locator('input[placeholder="0.0"]').first(); await inputField.fill('0.0001'); // 执行 swap await page.getByRole('button', { name: 'Swap' }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); // 处理支出上限批准 (Permit2) let notificationType = await metamask.identifyNotificationType(); if (notificationType === NotificationPageType.SpendingCap) { await metamask.handleAction(BaseActionType.CHANGE_SPENDING_CAP, { approvalType: ActionApprovalType.APPROVE, }); notificationType = await metamask.identifyNotificationType(); } // 处理 Permit2 的签名 if (notificationType === NotificationPageType.SpendingCap) { await metamask.handleAction(BaseActionType.HANDLE_SIGNATURE, { approvalType: ActionApprovalType.APPROVE, }); notificationType = await metamask.identifyNotificationType(); } // 处理实际的 swap 交易 if (notificationType === NotificationPageType.Transaction) { await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { approvalType: ActionApprovalType.APPROVE, }); } // 验证 swap 完成 await expect(page.getByRole('link', { name: 'View on Explorer' })).toBeVisible({ timeout: 10_000, }); }); });这说明了:
- 无需自定义钱包设置:OnchainTestKit 自动处理 MetaMask 的安装和配置
- Fork 测试:测试针对真实的 Base Sepolia 网络在部署了所有依赖合约的特定区块高度运行,但每个测试都获得其自己隔离的 fork
- 可靠的弹窗处理:复杂的钱包交互简化为简单的 handleAction 调用
- 智能 RPC 路由:前端使用固定的
localhost:8545,框架路由到正确的测试节点 - 自动清理:无需手动资源管理
在 OnchainTestKit 之前,同样的测试将需要数百行自定义钱包自动化代码。现在它简洁、可读且可维护。
使用 OnchainTestKit 进行生产 CI/CD
以下是我们在 Verified Pools CI 管道中大规模运行这些测试的方式:
## .github/workflows/playwright.yml jobs: e2e-tests: # 为了简洁起见,省略了其他设置步骤 # 安装 xvfb 用于无头浏览器测试 - name: Install xvfb run: | sudo apt-get update sudo apt-get install -y xvfb - name: Install dependencies run: yarn install --frozen-lockfile - name: Install Playwright Browsers run: yarn playwright install --with-deps - name: Build application run: yarn build env: NEXT_PUBLIC_BASE_SEPOLIA_RPC_URLS: http://localhost:8545 - name: Prepare MetaMask Extension run: yarn e2e:metamask:prepare # 并行运行交易测试,使用 10 个 workers - name: Run Playwright TX tests with xvfb run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" yarn playwright test --workers=10 --reporter=list,github env: E2E_TEST_SEED_PHRASE: ${{ secrets.E2E_TEST_SEED_PHRASE }}关键的生产功能:
- 并行执行:--workers=10 同时运行 10 个测试,每个测试都有隔离的区块链节点
- 环境隔离:每个测试都获得其自己的 Base Sepolia fork,没有冲突
- 强大的 CI 设置:使用 xvfb 处理无头浏览器自动化和适当的清理
结果:我们的 CI 在不到 10 分钟的时间内并行运行 100 多个全面的链上应用测试。
立即试用 OnchainTestKit
OnchainTestKit 现已可用,并已发布到 NPM。要开始使用,请查看 GitHub 仓库 中的文档和示例。
对改进链上开发者体验感兴趣?
如果你对增加链上开发者的影响力感兴趣,请了解我们的 Onchain DevX 团队很乐意与你见面! 我们正在招聘。
- 原文链接: blog.base.dev/introducin...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~