Testing apps
calimero-sdk’s testing feature is the harness: your app built as the real
calimero:app component, run on the real wasmtime engine with an in-memory
host (crates/sdk/src/testing). A passing test means what it means on a node.
The harness plays the node’s executor: it applies a call’s changes only when the
call ends Ok, seeds each logged run so tests repeat, and resolves event
audiences against the org you give it.
Set up
Section titled “Set up”[dependencies]calimero-sdk = { .. }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]calimero-sdk = { .., features = ["testing"] }calimero-app-build = { .. }serde_json = "1"calimero-app-build builds the crate into a component (wasm32-wasip2) for the
test. The component apps in core share a small helper for that
(apps/kv-store/tests/common).
TestApp
Section titled “TestApp”apps/kv-store/tests/harness.rs:
use calimero_sdk::testing::{Failure, Org, TestApp};use serde_json::json;
#[test]fn set_get_len_remove() { let mut t = common::app(Org::new().members(["bob"])); t.as_("owner").call("set", json!({ "key": "k", "value": "v" })).unwrap(); assert_eq!(t.as_("owner").view("get", json!({ "key": "k" })), Ok(json!("v"))); assert_eq!(t.as_("owner").view("len", json!({})), Ok(json!("1")));}TestApp::new(component, org, now_ms)creates the app;initruns as the owner.Org::new().admins([..]).members([..]).guests([..]).teams([..])sets the org the app sees throughenv::org_roleandenv::in_team.t.as_(account).call(method, args)/.view(..)run a method asaccount, with camelCase JSON arguments, the same as a client sends. The result isResult<Value, Failure>:Failure::App { code, fields }for an app error.t.events_for(account)lists the eventsaccountreceived, in order.t.anonymous()andt.system()call as a caller who is not signed in and as the node;t.api_key(..)as an API key.- Time and timers:
t.set_time(ms),t.advance(ms),t.tick(timer). - The outside world:
t.mock_http(..)answers outbound HTTP,t.mock_app(..)stands in for another app,t.take_xcalls()/t.deliver(..)inspect and deliver cross-app messages,t.add_file(..)/t.file(..)seed and read files.
Scenarios
Section titled “Scenarios”A scenario is a JSON list of steps that the Rust harness, the TypeScript
harness and the node’s cluster runner all run, so one file tests the app
everywhere. Each scenarios/<name>.json runs in the org its
<name>.org.json names.
[ { "as": "bob", "op": "call", "method": "getOrInsert", "args": { "key": "k", "value": "first" }, "expect": { "ok": "first" }, "events": { "bob": ["Inserted"] } }]op:call,view,tick(a timer, always as the system),advance(milliseconds),removeMember, orupgradein a migration scenario (below).as: an account,"@anonymous"or"@system";"confirmed": trueadds the passkey step-up.expect:{ "ok": value },{ "error": code, "fields": {..} },{ "trap": .. },{ "confirmRequired": true }, or{ "refused": "<API code>" }when the platform refuses before the app runs."save": "id"stores a result;"$id"later inargsorexpectis replaced by it."@bob"is the accountbobas the app sees it.eventslists, per account, every event name it has received so far.
Run them all from one test (apps/kv-store/tests/scenarios.rs):
use calimero_sdk::testing::run_scenario;
#[test]fn every_scenario_passes() { for path in common::scenarios() { let mut t = common::app(common::org_of(&path)); run_scenario(&mut t, &path).unwrap_or_else(|e| panic!("{}: {e:#}", path.display())); }}Upgrades: test against a shipped release’s state
Section titled “Upgrades: test against a shipped release’s state”An upgrade switches an app’s code and keeps its state, so the code you ship
next must read what every release still in use wrote
(Upgrades & state). Test that against the real
thing: the signed .mpk you shipped, not today’s code with an old version
number. The harness runs that release’s component, lets a scenario write state
with it, upgrades to the build under test as a node does (code only, nothing
runs) and goes on with the same state.
Lay it out by release, beside scenarios/:
releases/1.0.0.mpk the bundle 1.0.0 shipped, committed as it was publishedmigrations/1.0.0.json a scenario: steps on 1.0.0, { "op": "upgrade" }, steps on this buildmigrations/1.0.0.org.json its org, as for any scenario (optional)tests/migrations.rsapps/kv-store/migrations/0.1.0.json:
[ { "as": "owner", "op": "call", "method": "set", "args": { "key": "a", "value": "1" } }, { "as": "bob", "op": "call", "method": "set", "args": { "key": "b", "value": "2" } }, { "op": "upgrade" }, { "as": "bob", "op": "view", "method": "entries", "args": {}, "expect": { "ok": { "a": "1", "b": "2" } } }, { "as": "bob", "op": "call", "method": "updateIfExists", "args": { "key": "a", "value": "10" }, "expect": { "ok": true } }]apps/kv-store/tests/migrations.rs runs every one of them:
use calimero_sdk::testing::{release_component, run_migration};
#[test]fn every_release_upgrades_to_this_build() { let dir = Path::new(env!("CARGO_MANIFEST_DIR")); for path in common::scenarios_in(&dir.join("migrations")) { let version = path.file_stem().and_then(|s| s.to_str()).unwrap_or_default(); let mpk = std::fs::read(dir.join("releases").join(format!("{version}.mpk"))).unwrap(); let old = release_component(&mpk, None).unwrap(); run_migration(&old, common::component(), common::org_of(&path), common::T0, &path) .unwrap_or_else(|e| panic!("{}: {e:#}", path.display())); }}release_component(mpk, service)(featurereleases) is the component ofservice(Nonefor a single-service release) in a signed.mpk, after the signature and digest checks a node makes on install.run_migration(from, to, org, now_ms, path)starts the app onfrom, runs the scenario, and switches totoat its one{ "op": "upgrade" }step; saved variables carry across it. It returns theTestAppfor more asserts.t.upgrade(component)does the switch in a test of your own;Refused { 400, "BAD_BUNDLE" }for a component that cannot be prepared.
Turn the feature on beside testing:
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]calimero-sdk = { .., features = ["testing", "releases"] }When the new code cannot read the old state, the step that reads it fails and names it:
migrations/0.1.0.json: step 5: {"as":"bob","method":"entries","op":"view",..}:expected ok {"a":"1","b":"2"}, got Ok(Object {})Keep a release’s files for as long as an instance may still run it: an instance can skip releases, and every later build must read its state. The step-by-step walk through a release is Ship v2 of your app.
Unit tests on the host
Section titled “Unit tests on the host”Plain #[test]s may also call the app’s state directly on the host target:
env::mock stands in for the host there. Actions, cross-app calls and outbound
HTTP answer an error outside a component; use TestApp for those.