ic-testkit is a small wrapper and helper layer around pocket-ic, the local Internet Computer testing runtime this crate stands on. It does not replace pocket-ic; it adds reusable Rust test-harness conveniences on top of it.
Use pocket-ic directly when you want the underlying simulator/runtime API. Use ic-testkit when you want typed Candid calls, install helpers, serialized PocketIC startup, cached baselines, deterministic fake principals, wasm artifact utilities, and compact benchmark reporting.
[dev-dependencies]
ic-testkit = "0.1.8"Use PicSerialGuard when a test owns a PocketIC instance. Pic then provides a small Candid-aware wrapper for common calls.
use ic_testkit::pic::{acquire_pic_serial_guard, pic};
#[test]
fn calls_a_counter_canister() {
let _guard = acquire_pic_serial_guard();
let pic = pic();
let counter = install_counter(&pic);
let _: () = pic.update_call(counter, "increment", ()).unwrap();
let value: u64 = pic.query_call(counter, "get", ()).unwrap();
assert_eq!(value, 1);
}Use update_call_as and query_call_as when caller identity matters. In
tests that should fail immediately on transport, PocketIC, or Candid codec
errors, use update_call_or_panic, query_call_or_panic,
update_call_as_or_panic, or query_call_as_or_panic. These helpers only
unwrap the outer PicCallError; application-level return values such as
Result<T, E> are returned unchanged.
ic-testkit resolves the PocketIC server binary before starting pocket-ic.
By default it uses POCKET_IC_BIN when set, then checks a versioned cache path
under the system temp directory such as /tmp/pocket-ic-server-14.0.0/pocket-ic.
If the binary is missing, try_pic() returns PicStartError::BinaryUnavailable
with setup guidance instead of letting pocket-ic panic.
Supported configuration:
POCKET_IC_BIN=/trusted/path: use an existing ungzipped executable binaryIC_TESTKIT_ALLOW_POCKET_IC_DOWNLOAD=1: download the pinned server on cache miss
Recommended setup:
- CI: install or cache the pinned PocketIC binary and set
POCKET_IC_BINto that trusted executable path. - Local development: either set
POCKET_IC_BINto a local executable, or setIC_TESTKIT_ALLOW_POCKET_IC_DOWNLOAD=1when you are comfortable lettingic-testkitpopulate its versioned temp cache. - Preflight checks: call
try_ensure_pocket_ic_bin()from a small setup test or helper command to fail before starting a larger PocketIC suite.
Use PicRuntimeConfig when a test harness needs code-level control over the
cache directory or SHA-256 verification:
use ic_testkit::pic::{PicRuntimeConfig, try_ensure_pocket_ic_bin};
let binary = try_ensure_pocket_ic_bin()?;
let cached = PicRuntimeConfig::from_env()
.allow_download(true)
.cache_dir(".cache")
.ensure_binary()?;
# Ok::<(), Box<dyn std::error::Error>>(())Install a prebuilt wasm into a fresh PocketIC instance:
use ic_testkit::{artifacts, pic::install_prebuilt_canister};
#[test]
fn installs_a_prebuilt_canister() {
let workspace = artifacts::workspace_root_for(env!("CARGO_MANIFEST_DIR"));
let target = artifacts::test_target_dir(&workspace, "pic-wasm");
let wasm = artifacts::read_wasm(&target, "counter_canister", "release");
let fixture = install_prebuilt_canister(wasm, vec![]);
fixture.pic().tick();
}Use install_prebuilt_canister_from_spec when a standalone fixture needs an
explicit install sender or diagnostic label:
use candid::{Principal, encode_one};
use ic_testkit::pic::{InstallSpec, install_prebuilt_canister_from_spec};
let fixture = install_prebuilt_canister_from_spec(
InstallSpec::new(counter_wasm, encode_one(()).unwrap(), 1_000_000_000_000)
.install_sender(Principal::anonymous())
.label("counter"),
);For an existing Pic, use create_and_install_with_args or
try_create_and_install_with_args. Use InstallSpec when you want an explicit
install sender, a diagnostic label, or sequential batch installs:
use candid::encode_one;
use ic_testkit::pic::{InstallSpec, Pic};
fn install_pair(pic: &Pic, first_wasm: Vec<u8>, second_wasm: Vec<u8>) {
let ids = pic.create_and_install_many([
InstallSpec::new(first_wasm, encode_one(()).unwrap(), 1_000_000_000_000)
.label("first"),
InstallSpec::new(second_wasm, encode_one(()).unwrap(), 1_000_000_000_000)
.label("second"),
]);
assert_eq!(ids.len(), 2);
}Batch installs are sequential. If one install fails, earlier installs remain in
the PocketIC instance, the failed canister may also exist with the id exposed by
PicInstallError::canister_id(), and later installs are not attempted. If
PocketIC reports install-code rate limiting, retry_install_code_ok retries
while advancing PocketIC time.
Build wasm packages into a dedicated target directory and check expected artifacts:
use ic_testkit::artifacts;
let workspace = artifacts::workspace_root_for(env!("CARGO_MANIFEST_DIR"));
let target = artifacts::test_target_dir(&workspace, "pic-wasm");
artifacts::build_wasm_canisters(
&workspace,
&target,
&["counter_canister"],
&["--release"],
&[],
);
assert!(artifacts::wasm_artifacts_ready(
&target,
&["counter_canister"],
"release",
));There are also helpers for reading wasm files and checking generated .icp artifacts against watched inputs.
ic_testkit::benchmark turns compact canister log markers into parsed events, paired spans, aggregate rows, CSV files, and a Markdown summary. The default marker prefix is ICTK:
ICTK|<label>:start|<instructions>|<heap_bytes>|<memory_bytes>|<total_allocation>
ICTK|<label>:end|<instructions>|<heap_bytes>|<memory_bytes>|<total_allocation>
Parse, pair, and aggregate captured logs:
use ic_testkit::benchmark::{
aggregate_benchmark_spans, pair_benchmark_spans, parse_benchmark_events,
BenchmarkParserConfig,
};
let logs = "\
ICTK|app/myfunc/something:start|100|200|300|400
ICTK|app/myfunc/something:end|150|260|390|430
";
let parsed = parse_benchmark_events(logs, &BenchmarkParserConfig::default());
let spans = pair_benchmark_spans(&parsed.events);
let aggregates = aggregate_benchmark_spans(&spans.spans);
assert_eq!(aggregates.rows[0].span_label, "app/myfunc/something");The report writer emits CSV artifacts for raw events, spans, aggregates, malformed/unpaired/invalid markers, and comparisons, plus bench-summary.md and metadata.json. Run helpers create directories such as reports/runs/2026-05-24T162600Z-a1b2c3d-0001/ and discover compatible previous runs.
Call Performance::measure around the region under measurement:
use ic_testkit::performance::Performance;
Performance::measure("app/myfunc/something:start");
// code under measurement
Performance::measure("app/myfunc/something:end");The helper prints the compact ICTK|... line with the IC CDK call-context instruction counter, Wasm linear memory size, stable memory size, and a total_allocation slot. The in-repo canisters/test/perf_probe fixture tests this end to end.
For expensive setup, CachedPicBaseline can snapshot canisters once and restore them between tests. If the cached PocketIC instance has died, restore_or_rebuild_cached_pic_baseline rebuilds instead of reusing a broken instance.
use std::sync::Mutex;
use candid::Principal;
use ic_testkit::pic::{
CachedPicBaseline, Pic, restore_or_rebuild_cached_pic_baseline,
};
struct BaselineMetadata {
controller_id: Principal,
canister_id: Principal,
}
static BASELINE: Mutex<Option<CachedPicBaseline<BaselineMetadata>>> = Mutex::new(None);
fn baseline_for_test() {
let (baseline, cache_hit) = restore_or_rebuild_cached_pic_baseline(
&BASELINE,
|| build_baseline_once(),
|baseline| baseline.restore(baseline.metadata().controller_id),
);
if cache_hit {
baseline.pic().tick();
}
let canister_id = baseline.metadata().canister_id;
let value: u64 = baseline
.pic()
.query_call_or_panic(canister_id, "get", ());
assert_eq!(value, 0);
}
fn build_baseline_once() -> CachedPicBaseline<BaselineMetadata> {
let (pic, controller_id, canister_id) = build_expensive_fixture();
CachedPicBaseline::capture(
pic,
controller_id,
[canister_id],
BaselineMetadata {
controller_id,
canister_id,
},
)
.expect("snapshot capture must be available")
}
fn build_expensive_fixture() -> (Pic, Principal, Principal) {
unimplemented!("install the canisters needed by this test suite")
}Fake gives stable principals and account-like values from numeric seeds:
use ic_testkit::Fake;
let alice = Fake::principal(1);
let bob = Fake::principal(2);
let account = Fake::account(42);
assert_ne!(alice, bob);
assert_eq!(account.owner, Fake::principal(42));PicCandid query/update helpers with contextual errors and panic-on-transport variantsPicSerialGuardfor cross-process PocketIC serialization- generic wasm install helpers, retry helpers, diagnostics, and standalone fixtures
- cached snapshot baselines for expensive test setup
- deterministic fake principals and accounts
- wasm path/build/readiness helpers, including generated
.icpfreshness checks - compact benchmark marker parsing, aggregation, comparison, and report writing
- canister-side
Performance::measuremarker emission
This crate does not define application init payloads, endpoint names, role models, readiness polling, canister graph topology, benchmark labels, threshold policy, CI failure policy, or broad self-test orchestration. Those belong in the application or framework that owns the canisters being tested.
- MSRV: Rust 1.88
- internal build/test lane: Rust 1.96
make test
make test-canisters
make build-test-canisters
make release-check