name: evolve-testing description: Overview of Evolve SDK testing infrastructure including TestApp, SimTestApp, MockEnv, and transaction generators. Use when writing tests, setting up test harnesses, using test infrastructure, or choosing between testing approaches.
Evolve Testing Infrastructure
The Evolve SDK provides a layered testing infrastructure for unit tests, integration tests, stress testing, and property-based testing.
Test Harnesses
TestApp - Simple Unit Testing
Use for fast, isolated tests without full simulation:
use testapp::TestApp;
#[test]
fn test_basic_flow() {
let mut app = TestApp::new();
let accounts = app.accounts();
// Execute as a specific account
app.system_exec_as(accounts.alice, |env| {
let token = TokenRef::from(accounts.atom);
token.transfer(accounts.bob, 100, env)
}).unwrap();
// Advance blocks
app.next_block();
app.go_to_height(10);
// Mint test tokens
let asset = app.mint_atom(accounts.alice, 1000);
}
When to use: Unit tests, quick functional verification, no need for deterministic replay.
SimTestApp - Full Simulation
Use for integration tests with deterministic execution, tracing, and fault injection:
use testapp::SimTestApp;
use evolve_simulator::SimConfig;
#[test]
fn test_with_simulation() {
// Default config with seed 0
let mut app = SimTestApp::new();
// Or with custom config
let mut app = SimTestApp::with_config(SimConfig::default(), 42);
let accounts = app.accounts();
// Apply blocks directly
let block = TestBlock::new(1, vec![tx1, tx2]);
let result = app.apply_block(&block);
// Or generate blocks with closures
let results = app.run_blocks_with(10, |height, sim| {
vec![generate_tx(height, sim)]
});
// Access simulator for time/RNG
app.simulator().time().block_height();
app.simulator_mut().rng().gen_range(0..100);
}
When to use: Integration tests, multi-block scenarios, deterministic reproduction, trace capture.
Transaction Generators
For simulation-based testing, use the registry pattern:
use testapp::{TxGeneratorRegistry, TokenTransferGenerator};
#[test]
fn test_with_generators() {
let mut app = SimTestApp::new();
let accounts = app.accounts();
let mut registry = TxGeneratorRegistry::new();
// Register weighted generators
registry.register(10, TokenTransferGenerator::new(
accounts.atom,
vec![accounts.alice, accounts.bob], // senders
vec![accounts.alice, accounts.bob], // recipients
1, // min amount
100, // max amount
100000, // gas limit
));
// Run 100 blocks with generated transactions
let results = app.run_blocks_with_registry(100, &mut registry);
// Check results
for result in &results {
assert!(result.tx_results.iter().all(|r| r.response.is_ok()));
}
}
Custom Generator
use testapp::TxGenerator;
use evolve_simulator::Simulator;
struct MyGenerator {
// state
}
impl TxGenerator for MyGenerator {
fn generate_tx(&mut self, height: u64, sim: &mut Simulator) -> Option<TestTx> {
let amount = sim.rng().gen_range(1..=100);
Some(TestTx {
sender: self.sender,
recipient: self.target,
request: InvokeRequest::new(&MyMsg { amount }).ok()?,
gas_limit: 100000,
funds: vec![],
})
}
}
Trace Recording
Capture execution traces for debugging:
use evolve_debugger::{TraceBuilder, StateSnapshot};
#[test]
fn test_with_trace() {
let mut app = SimTestApp::new();
// Run with automatic trace capture
let (results, trace) = app.run_blocks_with_trace(10, |height, sim| {
vec![generate_tx(height, sim)]
});
// Or manual trace building
let snapshot = StateSnapshot::from_data(
app.simulator().storage().snapshot().data,
app.simulator().time().block_height(),
app.simulator().time().now_ms(),
);
let mut builder = TraceBuilder::new(app.simulator().seed_info().seed, snapshot);
let result = app.apply_block_with_trace(&block, &mut builder);
let trace = builder.finish();
// Save trace for later analysis
trace.save("test_trace.bin").unwrap();
}
Mock Environment
For testing modules in isolation without the STF:
use evolve_testing::MockEnv;
use evolve_core::AccountId;
#[test]
fn test_module_directly() {
let contract = AccountId::new(1);
let sender = AccountId::new(2);
let mut env = MockEnv::new(contract, sender);
let module = MyModule::default();
module.initialize(sender, &mut env).unwrap();
// Change sender for subsequent calls
env = env.with_sender(AccountId::new(3));
// Test unauthorized access
let result = module.admin_only(&mut env);
assert!(result.is_err());
}
MockEnv with Failure Injection
use evolve_collections::mocks::MockEnvironment;
let mut env = MockEnvironment::default();
env = env.with_failure(); // Next storage operation will fail
let result = module.some_operation(&mut env);
assert!(result.is_err());
Test Patterns
Setup Helper
fn setup_test_environment() -> (StorageMock, AccountStorageMock, CustomStf, GenesisAccounts) {
let bootstrap_stf = build_stf(SystemAccounts::placeholder(), PLACEHOLDER_ACCOUNT);
let mut codes = AccountStorageMock::default();
install_account_codes(&mut codes);
let mut state = StorageMock::default();
let (genesis_state, accounts) = do_genesis(&bootstrap_stf, &codes, &state).unwrap();
state.apply_changes(genesis_state.into_changes().unwrap()).unwrap();
let stf = build_stf(SystemAccounts::new(accounts.gas_service), accounts.scheduler);
(state, codes, stf, accounts)
}
Block Execution Test
#[test]
fn test_block_execution() {
let (mut storage, codes, stf, accounts) = setup_test_environment();
let tx = TestTx {
sender: accounts.alice,
recipient: accounts.atom,
request: InvokeRequest::new(&TransferMsg { to: accounts.bob, amount: 50 }).unwrap(),
gas_limit: 100000,
funds: vec![],
};
let block = TestBlock::new(1, vec![tx]);
let (result, new_state) = stf.apply_block(&storage, &codes, &block);
assert!(result.tx_results[0].response.is_ok());
storage.apply_changes(new_state.into_changes().unwrap()).unwrap();
}
State Verification
#[test]
fn test_state_changes() {
let mut app = TestApp::new();
let accounts = app.accounts();
// Get balance before
let before = app.system_exec_as(accounts.alice, |env| {
TokenRef::from(accounts.atom).get_balance(accounts.alice, env)
}).unwrap();
// Do transfer
app.system_exec_as(accounts.alice, |env| {
TokenRef::from(accounts.atom).transfer(accounts.bob, 100, env)
}).unwrap();
// Verify balance after
let after = app.system_exec_as(accounts.alice, |env| {
TokenRef::from(accounts.atom).get_balance(accounts.alice, env)
}).unwrap();
assert_eq!(before.unwrap() - after.unwrap(), 100);
}
Benchmarking
Use Criterion with iter_batched for proper setup/teardown:
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
fn bench_apply_block(c: &mut Criterion) {
let mut group = c.benchmark_group("block_execution");
for tx_count in [10, 100, 500] {
group.bench_function(format!("apply_block_{tx_count}_txs"), |b| {
b.iter_batched(
|| {
// Setup: create app and block
let mut app = SimTestApp::new();
let block = make_block(tx_count, &mut app);
(app, block)
},
|(mut app, block)| {
// Measured: apply block
app.apply_block(&block)
},
BatchSize::SmallInput,
)
});
}
group.finish();
}
criterion_group!(benches, bench_apply_block);
criterion_main!(benches);
When to Use What
| Scenario | Tool |
|---|---|
| Unit test a single module | MockEnv |
| Test module interactions | TestApp |
| Multi-block scenarios | SimTestApp |
| Deterministic reproduction | SimTestApp with fixed seed |
| Stress testing with faults | SimTestApp with SimConfig::stress_test() |
| Debug failing test | apply_block_with_trace + debugger |
| Property-based testing | evolve_proptest + SimTestApp |
| Performance benchmarks | Criterion + SimTestApp |
Files
crates/app/testapp/src/testing.rs- TestApp harnesscrates/app/testapp/src/sim_testing.rs- SimTestApp harnesscrates/app/sdk/testing/src/server_mocks.rs- Storage/code mockscrates/app/sdk/collections/src/mocks.rs- MockEnvironmentcrates/testing/simulator/- Deterministic simulatorcrates/testing/debugger/- Trace recording/replaycrates/testing/proptest/- Property-based testingcrates/app/testapp/benches/- Benchmark examples