Awesome-omni-skill midnight-dapp:testing-patterns
Use when writing unit tests for Midnight contract interaction code, integration testing without ZK proofs, E2E testing with Playwright or Cypress, or setting up CI/CD pipelines for Midnight DApps.
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/testing-security/midnight-dapp-testing-patterns" ~/.claude/skills/diegosouzapw-awesome-omni-skill-midnight-dapp-testing-patterns && rm -rf "$T"
manifest:
skills/testing-security/midnight-dapp-testing-patterns/SKILL.mdsource content
Testing Patterns
Test Midnight DApps efficiently using mocked providers, simulated wallets, and testnet integration strategies.
When to Use
- Writing unit tests for contract interaction code
- Integration testing without real ZK proof generation
- E2E testing with Playwright or Cypress
- Setting up CI/CD pipelines for Midnight DApps
- Testing wallet connection flows without browser extension
- Validating transaction flows before testnet deployment
Key Concepts
The Testing Challenge
Midnight DApps face unique testing challenges:
| Challenge | Why It Matters | Solution |
|---|---|---|
| Proof generation takes seconds | Tests would be too slow | Mock proof providers |
| Wallet requires browser extension | Can't run in CI/CD | Mock wallet provider |
| Private state is local only | Hard to verify in tests | Controlled test state |
| Testnet requires real infrastructure | Flaky in automation | Mock for unit tests, testnet for E2E |
Testing Pyramid for Midnight DApps
E2E (Testnet) / \ / Real proofs \ / Real wallet \ / Slow (~min) \ /____________________\ | Integration (Mocked) / \ / Mock proof provider \ / Mock wallet provider \ / Fast (~seconds) \ /______________________________\ | Unit Tests / \ / Pure logic \ / No providers \ / Fast (~ms) \ /____________________\
Mock vs Real: When to Use Each
| Test Type | Proof Provider | Wallet Provider | Use Case |
|---|---|---|---|
| Unit | N/A | N/A | Pure business logic |
| Component | Mock | Mock | UI components |
| Integration | Mock | Mock | Contract interactions |
| E2E (local) | Mock | Mock | Full user flows |
| E2E (testnet) | Real | Real (Lace) | Pre-deployment validation |
References
| Document | Description |
|---|---|
| mocking-proofs.md | Mock proof provider for fast tests |
| mock-wallet-provider.md | Simulating Lace wallet in tests |
| testnet-workflows.md | E2E testing against testnet |
| web3-comparison.md | Hardhat/Foundry testing vs Midnight |
Examples
| Example | Description |
|---|---|
| mock-proof-context/ | Mock proof provider and test utilities |
| mock-wallet/ | Fake wallet implementation for tests |
| e2e-testnet/ | Playwright E2E test against testnet |
Quick Start
1. Install Test Dependencies
pnpm add -D vitest @testing-library/react @playwright/test msw
2. Create Mock Proof Provider
import { createMockProofProvider } from "./mockProofProvider"; // Returns dummy proofs instantly (no ZK computation) const mockProofProvider = createMockProofProvider({ latencyMs: 10, // Simulate realistic timing });
3. Create Mock Wallet
import { MockWallet } from "./MockWallet"; const mockWallet = new MockWallet({ address: "addr_test1qz_mock_address_for_testing_purposes_xyz", balance: 1000000n, network: "testnet", }); // Inject into window for components that check window.midnight globalThis.window = { midnight: { mnLace: mockWallet.connector }, };
4. Write a Test
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("should complete transfer with mocked providers", async () => { render( <TransferButton wallet={mockWallet.api} proofProvider={mockProofProvider} recipient="addr_test1..." amount={100n} /> ); fireEvent.click(screen.getByText("Transfer")); // No actual proof generation - instant! await screen.findByText("Transfer Complete"); expect(mockWallet.getBalance()).toBe(900n); }); });
Common Patterns
Testing Contract State Reads
import { describe, it, expect } from "vitest"; import { createMockContract } from "./testUtils"; describe("Contract State", () => { it("should read balance from contract state", 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); }); });
Testing Witness Execution
import { describe, it, expect } from "vitest"; import { witnesses, createInitialPrivateState } from "../witnesses"; describe("Witnesses", () => { it("should return balance from private state", () => { const privateState = createInitialPrivateState(new Uint8Array(32)); privateState.balance = 1000n; const context = { privateState, setPrivateState: () => {} }; const balance = witnesses.get_balance(context); expect(balance).toBe(1000n); }); it("should throw for expired credential", () => { const privateState = createInitialPrivateState(new Uint8Array(32)); privateState.credentials.set("abc123", { expiry: BigInt(Date.now() / 1000 - 3600), // Expired 1 hour ago data: new Uint8Array(32), }); const context = { privateState, setPrivateState: () => {} }; expect(() => witnesses.get_credential(context, hexToBytes("abc123")) ).toThrow("expired"); }); });
Testing Error Handling
import { describe, it, expect, vi } from "vitest"; import { MockWallet } from "./MockWallet"; describe("Error Handling", () => { it("should handle user rejection", async () => { const wallet = new MockWallet(); wallet.rejectNextTransaction("User rejected"); await expect( wallet.api.submitTransaction(mockTx) ).rejects.toThrow("User rejected"); }); it("should handle proof server unavailable", async () => { const proofProvider = createMockProofProvider({ shouldFail: true, errorMessage: "Proof server unavailable", }); await expect( proofProvider.generateProof(mockCircuit, mockWitness) ).rejects.toThrow("Proof server unavailable"); }); });
Snapshot Testing for Disclosures
import { describe, it, expect } from "vitest"; import { render } from "@testing-library/react"; import { DisclosureModal } from "../DisclosureModal"; describe("DisclosureModal", () => { it("should render disclosure summary correctly", () => { const disclosures = [ { field: "age", label: "Your Age", value: "25" }, { field: "country", label: "Country", value: "US" }, ]; const { container } = render( <DisclosureModal disclosures={disclosures} onConfirm={() => {}} /> ); expect(container).toMatchSnapshot(); }); });
CI/CD Integration
GitHub Actions Example
name: Test 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 # E2E with mocks - fast, reliable - run: pnpm test:e2e:mock # Optional: E2E with testnet (slower, requires secrets) # - run: pnpm test:e2e:testnet # env: # TESTNET_FAUCET_KEY: ${{ secrets.TESTNET_FAUCET_KEY }}
Related Skills
- Understanding what to mock in proof generationproof-handling
- Understanding Lace wallet API to mockwallet-integration
- Testing error scenarioserror-handling
- Testing state synchronizationstate-management
Related Commands
- Validates test configuration/dapp-check
- Diagnose test failures/dapp-debug tests