PocketIC Setup Guide
This guide explains how to set up PocketIC for local canister testing.
What is PocketIC?
PocketIC is a lightweight, deterministic IC replica for testing Rust canisters locally. It provides:
- Fast test execution (~1s per test vs ~10s with dfx)
- Deterministic behavior (no timing issues)
- Easy setup (single binary, no daemon)
- Full IC behavior simulation
Installation
Option 1: Download from dfinity/pocketic (Recommended)
# Check latest release at https://github.com/dfinity/pocketic/releases
# Download for your platform:
# Linux (x86_64)
curl -sLO https://github.com/dfinity/pocketic/releases/download/v6.0.0/pocket-ic-x86_64-linux.gz
gunzip pocket-ic-x86_64-linux.gz
mv pocket-ic-x86_64-linux pocket-ic
chmod +x pocket-ic
# macOS (Apple Silicon)
curl -sLO https://github.com/dfinity/pocketic/releases/download/v6.0.0/pocket-ic-aarch64-darwin.gz
gunzip pocket-ic-aarch64-darwin.gz
mv pocket-ic-aarch64-darwin pocket-ic
chmod +x pocket-ic
# Move to canister repo
mv pocket-ic /path/to/your-canister-repo/Option 2: Copy from Existing Repo
If another canister repo already has the binary:
cp /home/coby/git/membership/pocket-ic /home/coby/git/your-new-canister/Add to .gitignore
The binary is 73MB and should NOT be committed:
echo "pocket-ic" >> .gitignoreCargo.toml Configuration
Add PocketIC as a dev dependency:
[dev-dependencies]
pocket-ic = "6"
candid = "0.10"
serde = { version = "1", features = ["derive"] }Test File Structure
Create tests/pocketic_smoke.rs:
use candid::{decode_one, encode_one, encode_args, Principal};
use pocket_ic::{PocketIc, WasmResult};
use serde::{Deserialize, Serialize};
// =============================================================================
// Helper Functions
// =============================================================================
fn unwrap_wasm_result(result: WasmResult) -> Vec<u8> {
match result {
WasmResult::Reply(bytes) => bytes,
WasmResult::Reject(msg) => panic!("Canister rejected call: {}", msg),
}
}
fn get_wasm_path() -> String {
// Try wasm/ first (pre-built), then target/ (fresh build)
let paths = [
"wasm/your_canister.wasm",
"target/wasm32-unknown-unknown/release/your_canister.wasm",
];
for path in paths {
if std::path::Path::new(path).exists() {
return path.to_string();
}
}
panic!("WASM not found. Run: cargo build --release --target wasm32-unknown-unknown");
}
fn setup() -> (PocketIc, Principal, Principal) {
let pic = PocketIc::new();
let wasm = std::fs::read(get_wasm_path()).expect("Failed to read WASM");
let controller = Principal::from_text("aaaaa-aa").unwrap();
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 2_000_000_000_000);
// Install with init args (adjust based on your canister)
pic.install_canister(
canister_id,
wasm,
encode_one(Some(vec![controller])).unwrap(),
None,
);
(pic, canister_id, controller)
}
// =============================================================================
// Tests
// =============================================================================
#[test]
fn test_health_check() {
let (pic, canister_id, _) = setup();
let response = pic.query_call(
canister_id,
Principal::anonymous(),
"health",
encode_one(()).unwrap(),
).unwrap();
let health: String = decode_one(&unwrap_wasm_result(response)).unwrap();
assert_eq!(health, "ok");
}Running Tests
Build WASM First
cargo build --release --target wasm32-unknown-unknownRun All PocketIC Tests
cargo testRun Specific Test
cargo test test_health_checkRun with Output
cargo test -- --nocaptureCommon Patterns
Testing Query Calls
let response = pic.query_call(
canister_id,
Principal::anonymous(), // or specific principal
"method_name",
encode_one(arg).unwrap(),
).unwrap();
let result: ReturnType = decode_one(&unwrap_wasm_result(response)).unwrap();Testing Update Calls
let response = pic.update_call(
canister_id,
caller_principal, // Who is calling
"method_name",
encode_args((arg1, arg2, arg3)).unwrap(),
).unwrap();
let result: Result<T, String> = decode_one(&unwrap_wasm_result(response)).unwrap();Testing Access Control
fn non_admin_principal() -> Principal {
Principal::from_text("2vxsx-fae").unwrap()
}
#[test]
fn test_admin_only_rejects_non_admin() {
let (pic, canister_id, _controller) = setup();
let response = pic.update_call(
canister_id,
non_admin_principal(), // Not an admin
"admin_only_method",
encode_one(()).unwrap(),
).unwrap();
let result: Result<(), String> = decode_one(&unwrap_wasm_result(response)).unwrap();
assert!(result.is_err());
}Testing Option Return Types
#[test]
fn test_get_not_found() {
let (pic, canister_id, _) = setup();
let response = pic.query_call(
canister_id,
Principal::anonymous(),
"get_item",
encode_one(999u64).unwrap(), // Non-existent ID
).unwrap();
let result: Option<Item> = decode_one(&unwrap_wasm_result(response)).unwrap();
assert!(result.is_none());
}Troubleshooting
"pocket-ic: command not found"
The binary must be in the repo root directory. PocketIC looks for it relative to the test execution directory.
WASM Not Found
Build the WASM first:
cargo build --release --target wasm32-unknown-unknownOr copy pre-built WASM:
mkdir -p wasm
cp target/wasm32-unknown-unknown/release/your_canister.wasm wasm/Candid Decoding Errors
Ensure your test type definitions exactly match the canister's .did file. Field names, enum variants, and types must match exactly.
Controller Verification Fails
PocketIC's controller verification differs slightly from production IC. If testing controller-only methods, ensure you're calling as the controller principal used during pic.create_canister().
References
- PocketIC GitHub
- PocketIC Rust Crate
- Example: auth-service tests (
auth-service/tests/pocketic_smoke.rs) - Example: dao-admin tests (
dao-admin/tests/pocketic_smoke.rs) - Example: membership tests (
membership/tests/pocketic_smoke.rs)