name: midnight-dapp:testing-patterns
description: 在编写Midnight合约交互代码的单元测试、无需ZK证明的集成测试、使用Playwright或Cypress的端到端测试,或为Midnight DApps设置CI/CD流水线时使用。
测试模式
使用模拟提供者、模拟钱包和测试网集成策略来高效测试Midnight DApps。
使用时机
- 为合约交互代码编写单元测试
- 无需真实ZK证明生成的集成测试
- 使用Playwright或Cypress进行端到端测试
- 为Midnight DApps设置CI/CD流水线
- 测试无需浏览器扩展的钱包连接流程
- 在部署到测试网前验证交易流程
核心概念
测试挑战
Midnight DApps面临独特的测试挑战:
| 挑战 |
重要性 |
解决方案 |
| 证明生成需要数秒 |
测试会变得太慢 |
模拟证明提供者 |
| 钱包需要浏览器扩展 |
无法在CI/CD中运行 |
模拟钱包提供者 |
| 私有状态仅本地存储 |
测试中难以验证 |
受控的测试状态 |
| 测试网需要真实基础设施 |
自动化中不稳定 |
单元测试用模拟,端到端测试用测试网 |
Midnight DApps的测试金字塔
端到端测试 (测试网)
/ \
/ 真实证明 \
/ 真实钱包 \
/ 缓慢 (~分钟) \
/____________________\
|
集成测试 (模拟)
/ \
/ 模拟证明提供者 \
/ 模拟钱包提供者 \
/ 快速 (~秒) \
/______________________________\
|
单元测试
/ \
/ 纯业务逻辑 \
/ 无需提供者 \
/ 快速 (~毫秒) \
/____________________\
模拟 vs 真实:何时使用
| 测试类型 |
证明提供者 |
钱包提供者 |
使用场景 |
| 单元测试 |
不适用 |
不适用 |
纯业务逻辑 |
| 组件测试 |
模拟 |
模拟 |
UI组件 |
| 集成测试 |
模拟 |
模拟 |
合约交互 |
| 端到端测试 (本地) |
模拟 |
模拟 |
完整用户流程 |
| 端到端测试 (测试网) |
真实 |
真实 (Lace钱包) |
部署前验证 |
参考文档
示例
快速开始
1. 安装测试依赖
pnpm add -D vitest @testing-library/react @playwright/test msw
2. 创建模拟证明提供者
import { createMockProofProvider } from "./mockProofProvider";
// 即时返回虚拟证明(无需ZK计算)
const mockProofProvider = createMockProofProvider({
latencyMs: 10, // 模拟真实时间
});
3. 创建模拟钱包
import { MockWallet } from "./MockWallet";
const mockWallet = new MockWallet({
address: "addr_test1qz_mock_address_for_testing_purposes_xyz",
balance: 1000000n,
network: "testnet",
});
// 注入到window对象,供检查window.midnight的组件使用
globalThis.window = {
midnight: { mnLace: mockWallet.connector },
};
4. 编写测试
import { describe, it, expect, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { MockWallet } from "./MockWallet";
import { createMockProofProvider } from "./mockProofProvider";
import { TransferButton } from "../TransferButton";
describe("TransferButton", () => {
let mockWallet: MockWallet;
let mockProofProvider: MockProofProvider;
beforeEach(() => {
mockWallet = new MockWallet({ balance: 1000n });
mockProofProvider = createMockProofProvider();
});
it("应该使用模拟提供者完成转账", async () => {
render(
<TransferButton
wallet={mockWallet.api}
proofProvider={mockProofProvider}
recipient="addr_test1..."
amount={100n}
/>
);
fireEvent.click(screen.getByText("转账"));
// 没有实际的证明生成 - 瞬间完成!
await screen.findByText("转账完成");
expect(mockWallet.getBalance()).toBe(900n);
});
});
常见模式
测试合约状态读取
import { describe, it, expect } from "vitest";
import { createMockContract } from "./testUtils";
describe("合约状态", () => {
it("应该从合约状态读取余额", async () => {
const contract = createMockContract({
state: {
balances: new Map([["addr_test1...", 500n]]),
totalSupply: 10000n,
},
});
const balance = await contract.state.balances.get("addr_test1...");
expect(balance).toBe(500n);
});
});
测试见证执行
import { describe, it, expect } from "vitest";
import { witnesses, createInitialPrivateState } from "../witnesses";
describe("见证", () => {
it("应该从私有状态返回余额", () => {
const privateState = createInitialPrivateState(new Uint8Array(32));
privateState.balance = 1000n;
const context = { privateState, setPrivateState: () => {} };
const balance = witnesses.get_balance(context);
expect(balance).toBe(1000n);
});
it("应该为过期的凭证抛出错误", () => {
const privateState = createInitialPrivateState(new Uint8Array(32));
privateState.credentials.set("abc123", {
expiry: BigInt(Date.now() / 1000 - 3600), // 1小时前过期
data: new Uint8Array(32),
});
const context = { privateState, setPrivateState: () => {} };
expect(() =>
witnesses.get_credential(context, hexToBytes("abc123"))
).toThrow("已过期");
});
});
测试错误处理
import { describe, it, expect, vi } from "vitest";
import { MockWallet } from "./MockWallet";
describe("错误处理", () => {
it("应该处理用户拒绝", async () => {
const wallet = new MockWallet();
wallet.rejectNextTransaction("用户拒绝");
await expect(
wallet.api.submitTransaction(mockTx)
).rejects.toThrow("用户拒绝");
});
it("应该处理证明服务器不可用", async () => {
const proofProvider = createMockProofProvider({
shouldFail: true,
errorMessage: "证明服务器不可用",
});
await expect(
proofProvider.generateProof(mockCircuit, mockWitness)
).rejects.toThrow("证明服务器不可用");
});
});
披露信息的快照测试
import { describe, it, expect } from "vitest";
import { render } from "@testing-library/react";
import { DisclosureModal } from "../DisclosureModal";
describe("DisclosureModal", () => {
it("应该正确渲染披露摘要", () => {
const disclosures = [
{ field: "age", label: "您的年龄", value: "25" },
{ field: "country", label: "国家", value: "US" },
];
const { container } = render(
<DisclosureModal disclosures={disclosures} onConfirm={() => {}} />
);
expect(container).toMatchSnapshot();
});
});
CI/CD集成
GitHub Actions示例
name: 测试Midnight DApp
on: [push, pull_request]
jobs:
unit-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: pnpm install
- run: pnpm test:unit
- run: pnpm test:integration
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: pnpm install
- run: pnpm exec playwright install --with-deps
# 使用模拟的端到端测试 - 快速、可靠
- run: pnpm test:e2e:mock
# 可选:使用测试网的端到端测试(较慢,需要密钥)
# - run: pnpm test:e2e:testnet
# env:
# TESTNET_FAUCET_KEY: ${{ secrets.TESTNET_FAUCET_KEY }}
相关技能
proof-handling - 理解在证明生成中需要模拟什么
wallet-integration - 理解Lace钱包API以便模拟
error-handling - 测试错误场景
state-management - 测试状态同步
相关命令
/dapp-check - 验证测试配置
/dapp-debug tests - 诊断测试失败原因