Skip to content

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

bash
# 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:

bash
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:

bash
echo "pocket-ic" >> .gitignore

Cargo.toml Configuration

Add PocketIC as a dev dependency:

toml
[dev-dependencies]
pocket-ic = "6"
candid = "0.10"
serde = { version = "1", features = ["derive"] }

Test File Structure

Create tests/pocketic_smoke.rs:

rust
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

bash
cargo build --release --target wasm32-unknown-unknown

Run All PocketIC Tests

bash
cargo test

Run Specific Test

bash
cargo test test_health_check

Run with Output

bash
cargo test -- --nocapture

Common Patterns

Testing Query Calls

rust
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

rust
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

rust
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

rust
#[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:

bash
cargo build --release --target wasm32-unknown-unknown

Or copy pre-built WASM:

bash
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)

Hello World Co-Op DAO