MidnightDApp测试模式Skill midnight-dapp:testing-patterns

Midnight DApp测试模式是一套针对基于零知识证明的区块链去中心化应用(DApp)的完整测试方法论和最佳实践。它解决了ZK DApp特有的测试挑战,如证明生成耗时、钱包依赖浏览器扩展、私有状态难以验证等。该技能涵盖了从单元测试、集成测试到端到端测试的全流程,提供了模拟证明提供者、模拟钱包、测试网集成等关键解决方案,并包含CI/CD流水线配置指南。关键词:Midnight DApp测试,ZK证明模拟,区块链测试,DApp集成测试,智能合约测试,测试金字塔,CI/CD自动化,模拟钱包,测试网部署。

DApp开发 0 次安装 0 次浏览 更新于 2/26/2026

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钱包) 部署前验证

参考文档

文档 描述
mocking-proofs.md 用于快速测试的模拟证明提供者
mock-wallet-provider.md 在测试中模拟Lace钱包
testnet-workflows.md 针对测试网的端到端测试
web3-comparison.md Hardhat/Foundry测试与Midnight测试对比

示例

示例 描述
mock-proof-context/ 模拟证明提供者和测试工具
mock-wallet/ 用于测试的假钱包实现
e2e-testnet/ 针对测试网的Playwright端到端测试

快速开始

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 - 诊断测试失败原因