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
Examples
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
proof-handling - Understanding what to mock in proof generation
wallet-integration - Understanding Lace wallet API to mock
error-handling - Testing error scenarios
state-management - Testing state synchronization
Related Commands
/dapp-check - Validates test configuration
/dapp-debug tests - Diagnose test failures