# ic-testkit
#### A small wrapper and helper layer around <code>pocket-ic</code> for Internet Computer canister tests.
<p align="center">
<a href="https://crates.io/crates/ic-testkit"><img src="https://img.shields.io/crates/v/ic-testkit.svg" alt="Crates.io"></a>
<a href="https://docs.rs/ic-testkit"><img src="https://docs.rs/ic-testkit/badge.svg" alt="Docs.rs"></a>
<a href="https://crates.io/crates/ic-testkit"><img src="https://img.shields.io/crates/d/ic-testkit.svg" alt="Downloads"></a>
<a href="Cargo.toml"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
<a href="Cargo.toml"><img src="https://img.shields.io/badge/MSRV-1.88.0-blue.svg" alt="MSRV"></a>
<a href="README.md#toolchains"><img src="https://img.shields.io/badge/internal%20rust-1.95.0-orange.svg" alt="Internal Rust"></a>
<a href="Cargo.toml"><img src="https://img.shields.io/badge/edition-2024-purple.svg" alt="Rust edition"></a>
<a href="Cargo.toml"><img src="https://img.shields.io/badge/PocketIC-13.0-green.svg" alt="PocketIC"></a>
<a href="https://github.com/dragginzgame/ic-testkit"><img src="https://img.shields.io/badge/GitHub-dragginzgame%2Fic--testkit-black.svg" alt="Repository"></a>
</p>
`ic-testkit` is a wrapper around [`pocket-ic`](https://crates.io/crates/pocket-ic), the core local IC testing runtime this crate builds on. It does not replace `pocket-ic`; it adds a small, opinionated host-side layer for test suites that want typed Candid calls, install helpers, diagnostics, serialized PocketIC startup, cached baselines, deterministic fake principals, and wasm artifact utilities.
<p align="center">
<img src="images/cave.png" alt="ic-testkit banner" width="640">
</p>
If you need the underlying IC simulator/runtime itself, start with [`pocket-ic`](https://crates.io/crates/pocket-ic). Use `ic-testkit` when you want reusable Rust test harness conveniences on top of it.
It is intentionally application-neutral. Bring your own init payloads, method names, readiness checks, fixture graph, and product-specific test policy.
## Install
```toml
[dev-dependencies]
ic-testkit = "0.0.4"
```
> [!WARNING]
> Do not use - some of this may be hallucinations, our best agents are currently auditing the code.
## Quick Start
Use `PicSerialGuard` when a test owns a PocketIC instance. It serializes PocketIC usage across processes, which helps avoid shared server/resource exhaustion in larger test runs.
```rust
use ic_testkit::pic::{acquire_pic_serial_guard, pic};
#[test]
fn starts_a_pic_instance() {
let _guard = acquire_pic_serial_guard();
let pic = pic();
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 1_000_000_000_000);
pic.tick();
}
```
## Calling Canisters
`Pic` wraps common update/query calls with Candid encoding and decoding. The error includes the canister id and method name when PocketIC rejects the call.
Examples marked `no_run` assume your test harness supplies the application-specific wasm, install helper, or fixture setup.
```rust,no_run
use ic_testkit::pic::{acquire_pic_serial_guard, pic};
#[test]
fn calls_a_counter_canister() {
let _guard = acquire_pic_serial_guard();
let pic = pic();
// Provide this from your own test harness. It should install a wasm module
// and return the installed canister id.
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 the `_as` variants when the caller matters:
```rust,no_run
use candid::Principal;
// `pic` is an ic_testkit::pic::Pic from your test setup.
let caller = ic_testkit::Fake::principal(7);
let ledger_id = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
let balance: u128 = pic
.query_call_as(ledger_id, caller, "balance_of", (caller,))
.unwrap();
```
## Installing Wasm
For one-off tests, install a prebuilt wasm into a fresh PocketIC instance:
```rust,no_run
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();
}
```
For existing `Pic` instances, use the lower-level helper:
```rust,no_run
// `pic` is an ic_testkit::pic::Pic from your test setup.
// `wasm` is a Vec<u8> containing the compiled canister wasm.
let canister_id = pic.create_and_install_with_args(
wasm,
candid::encode_one(()).unwrap(),
1_000_000_000_000,
);
```
If PocketIC reports install-code rate limiting, retry while advancing PocketIC time between attempts:
```rust,no_run
use std::time::Duration;
// `pic` is an ic_testkit::pic::Pic from your test setup.
// `wasm` is a Vec<u8> containing the compiled canister wasm.
.map_err(|err| err.to_string())
});
```
## Artifact Helpers
Build wasm packages into a dedicated target directory:
```rust,no_run
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",
));
```
Check generated `.icp` artifacts against watched inputs:
```rust,no_run
use ic_testkit::artifacts;
let workspace = artifacts::workspace_root_for(env!("CARGO_MANIFEST_DIR"));
let ready = artifacts::icp_artifact_ready_for_build(
&workspace,
".icp/local/canisters/counter/counter.wasm.gz",
&["Cargo.toml", "src"],
);
```
## Deterministic Test Identities
`Fake` gives stable principals and account-like values from numeric seeds:
```rust
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));
```
## Cached Baselines
For expensive multi-canister 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 this when setup time dominates the test and the fixture can be restored from PocketIC snapshots. Keep application-specific topology and readiness logic in your own test harness.
## What This Adds Over `pocket-ic`
- `Pic`, a narrow wrapper for the PocketIC operations used by this crate
- typed startup errors for common PocketIC launch failures
- `PicSerialGuard` for cross-process PocketIC serialization
- Candid query/update helpers with contextual errors
- generic wasm install helpers and install-code retry helpers
- canister status/log diagnostics
- standalone prebuilt-wasm fixtures
- cached snapshot baselines
- deterministic fake principals and accounts
- wasm path/build/readiness helpers
- watched-input freshness checks for generated `.icp` artifacts
## Boundaries
This crate does not define application init payloads, endpoint names, role models, readiness polling, canister graph topology, attestation policy, or broad self-test orchestration. Those belong in the application or framework that owns the canisters being tested.
## Toolchains
- MSRV: Rust 1.88
- internal build/test lane: Rust 1.95
## Local Checks
```sh
make test
cargo +1.88.0 check
cargo publish --dry-run
```