Contract Testing
Overview
Section titled “Overview”vm-test-harness provides an in-memory host that lets you execute contracts
locally without running a node. It’s ideal for unit-style tests, storage
fixtures, event assertions, and cross-contract call flows.
Key pieces:
TestHost: In-memory storage, logs, balances, block contextContractHarness: Load and execute Zig/Rust contract ELFsTestAccounts: Deterministic test addresses (alice, bob, carol, dave, eve, treasury)- Helpers: Calldata builders, result parsers, event assertions, storage fixtures, snapshots
Import
Section titled “Import”The crate lives in the workspace:
use vm_test_harness::{ ContractHarness, TestHost, TestAccounts, StorageFixture, build_calldata, build_calldata_no_args, parse_result, assert_call_ok, assert_event_emitted, assert_event_count, assert_storage_u128,};Build Artifacts First
Section titled “Build Artifacts First”You must build contract ELFs before loading them in tests:
# Zigcd contracts/my_zig && zig build -Doptimize=ReleaseSmall
# Rustjust contract-build --manifest-path contracts/my_rust/Cargo.toml
# All contractsjust contract-build-allQuick Start
Section titled “Quick Start”#[test]fn my_token_initializes() { // Skip gracefully if ELF not built yet let Some(harness) = ContractHarness::from_zig_artifact_or_skip( "contracts/my_token/zig-out/bin/my_token", "contracts/my_token", ).unwrap() else { return };
let mut host = TestHost::new(); let accounts = TestAccounts::new(); host.seed_balances(&accounts, 100_000);
// Build calldata: 4-byte selector + borsh-encoded args let calldata = build_calldata(SELECTOR_INITIALIZE, &InitArgs { fee_bps: 30 }); let result = harness.call( &mut host, accounts.alice, // origin b"my_token", // contract address &calldata, 1_000_000, // gas limit );
assert_call_ok(&result, "init should succeed"); assert_event_emitted(&host, "Initialized()"); assert_storage_u128(&host, b"my_token", b"fee_bps", 30);}Test Accounts
Section titled “Test Accounts”TestAccounts provides six deterministic addresses derived from
keccak256(label):
let accounts = TestAccounts::new();// accounts.alice, accounts.bob, accounts.carol,// accounts.dave, accounts.eve, accounts.treasuryCalldata & Results
Section titled “Calldata & Results”Calldata format: selector (4 bytes) || borsh(args).
// With argumentslet calldata = build_calldata(SELECTOR_TRANSFER, &TransferArgs { to, amount });
// Without argumentslet calldata = build_calldata_no_args(SELECTOR_TOTAL_SUPPLY);Parse return data:
let result = harness.call(&mut host, origin, contract, &calldata, gas);assert_call_ok(&result, "call should succeed");
// Typed result parsinglet supply: u128 = parse_result(&result.outcome.return_data)?;Storage Fixtures
Section titled “Storage Fixtures”use vm_test_harness::{TestHost, StorageFixture, assert_storage_u128, assert_storage_empty};
let mut host = TestHost::new();StorageFixture::new(&mut host) .contract(b"amm_pool") .set_u128(b"reserve0", 1_000_000) .set_u128(b"reserve1", 500_000) .set_u64(b"swap_fee_bps", 30) .done();
// ... execute swap ...assert_storage_u128(&host, b"amm_pool", b"reserve0", 900_000);assert_storage_empty(&host, b"amm_pool", b"pending_admin");Event Assertions
Section titled “Event Assertions”use vm_test_harness::{TestHost, assert_event_emitted, assert_event_with_topics, assert_event_count};use vm_test_harness::topic_from_u128;
let host = TestHost::new();// ... execute contract ...
assert_event_emitted(&host, "Transfer(address,address,uint256)");let from = [1u8; 32];let to = [2u8; 32];let amount = topic_from_u128(1000);assert_event_with_topics(&host, "Transfer(address,address,uint256)", &[from, to, amount]);assert_event_count(&host, 1);Cross‑Contract Calls
Section titled “Cross‑Contract Calls”Register multiple programs with the host and call by address:
let mut host = TestHost::new();let a = ContractHarness::from_zig_artifact("contracts/a/zig-out/bin/a").unwrap();let b = ContractHarness::from_zig_artifact("contracts/b/zig-out/bin/b").unwrap();
host.register_program(b"contract_a", a.program().clone());host.register_program(b"contract_b", b.program().clone());Then use normal ContractHarness::call flows to exercise call graphs.
Snapshots
Section titled “Snapshots”let mut host = TestHost::new();let snapshot = host.snapshot();// mutate state ...host.restore_snapshot(snapshot);Block Advancement
Section titled “Block Advancement”Advance the host’s block context between calls:
host.advance_block(); // increments block_number, updates timestampZig-Side Testing
Section titled “Zig-Side Testing”The Ashen SDK includes a Zig testing module for unit tests that run inside
zig test (no Rust harness needed):
const sdk = @import("ashen-sdk");const testing = sdk.testing;
test "balance updates correctly" { testing.assertStorageU128("balance", 1000); testing.assertEventEmitted("Transfer(address,address,uint256)"); testing.assertEqU128(actual, expected);}The Zig testing_host provides an in-memory storage backend with mock syscalls,
callable via testing_host.init() / testing_host.deinit().
Running Tests
Section titled “Running Tests”# All contract integration testscargo test -p vm-runtime
# Single contractcargo test -p vm-runtime --test amm_pool_v1
# Single test functioncargo test -p vm-runtime --test amm_pool_v1 -- amm_pool_initial_liquidity
# With outputcargo test -p vm-runtime --test amm_pool_v1 -- --nocaptureTests that use from_zig_artifact_or_skip will print a build hint and skip
gracefully if the ELF artifact is missing.
- Use
from_zig_artifact_or_skip()instead offrom_zig_artifact()to avoid test failures when artifacts are not built. - Use
ResourceLimitsto simulate production caps. - Use
snapshot()/restore_snapshot()to test rollback behavior. - Prefer deterministic fixtures for reproducible tests.
- Treat harness tests as fast unit tests, and use devnet tests for end-to-end.
Related
Section titled “Related”- Deploying Contracts — building artifacts
- Zig Guide — Zig contract authoring
- Rust Guide — Rust contract authoring
- Precompiles & Syscalls — available syscalls
- Debugging — debugging contract execution