Testing Midnight Contracts
Test Compact smart contracts using simulators and test frameworks.
Quick Start
import { ContractSimulator } from '@midnight-ntwrk/compact-simulator';
// Create simulator const simulator = new ContractSimulator(compiledContract);
// Call circuit const result = await simulator.call('increment', {});
// Check state expect(simulator.ledger.counter).toBe(1n);
Reference Files
Topic Resource
Simulator Setup references/simulator-setup.md
Test Patterns references/test-patterns.md
Debugging references/debugging.md
Test Environment
┌──────────────────────────────────────────────┐ │ Test Environment │ ├──────────────────────────────────────────────┤ │ ┌────────────────┐ ┌────────────────┐ │ │ │ Contract │ │ Contract │ │ │ │ Simulator │ │ Artifacts │ │ │ └────────────────┘ └────────────────┘ │ │ │ │ │ │ └────────┬─────────┘ │ │ ▼ │ │ ┌────────────────┐ │ │ │ Test Suite │ │ │ │ (Jest/Vitest)│ │ │ └────────────────┘ │ └──────────────────────────────────────────────┘
Installation
npm install -D @midnight-ntwrk/compact-simulator vitest
Basic Test Structure
import { describe, it, expect, beforeEach } from 'vitest'; import { ContractSimulator } from '@midnight-ntwrk/compact-simulator'; import { setNetworkId, NetworkId } from '@midnight-ntwrk/midnight-js-network-id';
describe('MyContract', () => { let simulator: ContractSimulator;
beforeEach(() => { setNetworkId(NetworkId.Undeployed); simulator = new ContractSimulator(compiledContract); });
it('should initialize with zero', async () => { expect(simulator.ledger.counter).toBe(0n); });
it('should increment counter', async () => { await simulator.call('increment', {}); expect(simulator.ledger.counter).toBe(1n); }); });
Testing Patterns
State Verification
it('should update ledger state', async () => { // Initial state expect(simulator.ledger.message).toBe('');
// Call circuit await simulator.call('setMessage', { input: 'Hello' });
// Verify state expect(simulator.ledger.message).toBe('Hello'); });
Error Testing
it('should reject invalid input', async () => { await expect(simulator.call('withdraw', { amount: 1000n })).rejects.toThrow('Assertion failed'); });
Privacy Testing
it('should not reveal private inputs', async () => { const result = await simulator.call('checkBalance', { balance: 1000n, required: 500n, });
// Result is boolean, not actual balance expect(result).toBe(true); // Ledger should not contain balance expect(simulator.ledger.balance).toBeUndefined(); });
Best Practices
-
✅ Test each circuit function independently
-
✅ Verify state changes after each call
-
✅ Test error conditions and edge cases
-
✅ Mock witnesses for privacy testing
-
✅ Use NetworkId.Undeployed for testing
-
❌ Don't test proof generation (use simulator)
-
❌ Don't rely on network in unit tests
Test Categories
Category Tests
Unit Individual circuit functions
Integration Multi-circuit workflows
State Ledger state transitions
Error Assertion failures
Privacy Data not leaked
Running Tests
Run all tests
npm test
Run with coverage
npm test -- --coverage
Run specific test file
npm test -- counter.test.ts
Watch mode
npm test -- --watch