本文介绍了OnchainTestKit,一个旨在简化和提高链上应用测试可靠性的工具。它通过自动化的钱包管理、可靠的弹窗处理、真正的测试隔离以及确定性的合约部署,解决了传统测试框架在应对去中心化应用时的挑战。该工具已在Verified Pools项目中成功应用,显著减少了测试所需的代码量,并提高了CI/CD的效率。
测试去中心化应用一直与测试传统的 web 应用有根本的不同。传统的应用处理的是直接的 HTTP 请求和可预测的数据库状态,而链上应用必须驾驭区块链交互的复杂世界:钱包连接、交易批准、网络切换和链状态。每一个交互都带来了现有测试框架未被设计用于处理的独特挑战。
在为 Verified Pools 项目 编写端到端测试时,我们遇到了许多这样的挑战。这个复杂的 DeFi 应用将机构级的流动性基础设施带到了链上市场,需要对前端、后端和智能合约集成之间的复杂用户流程进行测试。例如,用户连接他们的钱包,批准 token 支出,签署 Permit2 消息,向池提供流动性,并在多个合约中管理他们的头寸。原本应该是一个简单的测试工作,却迅速变成了一个长达数周的技术挑战和生产力瓶颈的历程。
测试去中心化应用引入了 web 自动化和区块链交互交叉处的复杂性。标准的端到端测试框架并非天生为此环境而构建,这迫使开发者在编写有效的测试之前解决几个根本问题。
根据我们的经验,有 4 个主要的测试障碍:
1. 钱包扩展设置 每一个测试都需要从头开始安装、配置和提供资金的浏览器钱包。这个过程对每个钱包(如 MetaMask 或 Coinbase Wallet)都是唯一的,并可能导致脆弱的自定义脚本。
2. 不可预测的钱包弹窗 在测试过程中,用户操作会触发钱包弹窗,用于交易批准、消息签名和网络切换。这些弹窗以不一致的时间和 UI 模式异步出现,导致不可预测性和不稳定性。
3. 共享链上状态 在共享区块链上,真正的测试并行是不可能的。当多个测试同时运行时,它们使用相同的钱包地址,并与同一区块链上的相同智能合约进行交互。这导致测试相互干扰,导致难以调试的不可预测的故障。
4. 合约部署和状态管理 在 Solidity 中的智能合约开发(使用 Foundry)和 TypeScript 中的端到端测试之间存在着一个根本的工具缺口。测试需要以特定的初始状态部署合约,但合约地址是不确定的,这破坏了与前端的连接。
OnchainTestKit 直接针对这四个关键问题,提供了专门构建的解决方案:
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();
结果:对开发者友好,与钱包无关,且易于使用。我们将钱包设置和管理的所有复杂性都隐藏在开发者之外。
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 使用智能等待策略,理解区块链的时间模式和钱包行为。内置的重试机制自动处理瞬时故障,而自适应超时则根据网络状况和弹窗复杂性进行调整。
结果:测试可靠地通过。不再有因时间问题或不可预测的钱包行为导致的不稳定测试。
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 时间从数小时缩短到数分钟。
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 如何将复杂的链上应用测试转化为简洁、可读的测试:
// 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 中的完整用户旅程:
// 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,
});
});
});
这说明了:
localhost:8545
,框架路由到正确的测试节点在 OnchainTestKit 之前,同样的测试将需要数百行自定义钱包自动化代码。现在它简洁、可读且可维护。
以下是我们在 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 }}
关键的生产功能:
结果:我们的 CI 在不到 10 分钟的时间内并行运行 100 多个全面的链上应用测试。
OnchainTestKit 现已可用,并已发布到 NPM。要开始使用,请查看 GitHub 仓库 中的文档和示例。
如果你对增加链上开发者的影响力感兴趣,请了解我们的 Onchain DevX 团队很乐意与你见面! 我们正在招聘。
- 原文链接: blog.base.dev/introducin...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!