PocketIC is a local canister testing solution for the Internet Computer. It provides a Rust library that works with the PocketIC server, allowing you to interact with local IC instances and test your canisters with native Rust functions.
Overview
PocketIC makes testing Internet Computer canisters as simple as calling Rust functions. It provides a full local IC environment without requiring a live network connection.
Installation
1. Install the PocketIC Rust Library
Add PocketIC to your project:
Or add it to your Cargo.toml:
[dev-dependencies]
pocket-ic = "12.0.0"
2. Download PocketIC Server
Download the compatible PocketIC server binary:
- Visit the PocketIC releases
- Download the binary compatible with your library version (check compatibility matrix)
- Extract the downloaded file
- On Unix systems, make it executable:
chmod +x pocket-ic
Specify the server binary location using either:
Environment variable:
export POCKET_IC_BIN=/path/to/pocket-ic
Or in code:
use pocket_ic::PocketIcBuilder;
let pic = PocketIcBuilder::new()
.with_server_binary("/path/to/pocket-ic")
.build();
Quick Start
Here’s a simple example testing a counter canister:
use candid::{Principal, encode_one};
use pocket_ic::PocketIc;
// 2T cycles
const INIT_CYCLES: u128 = 2_000_000_000_000;
#[test]
fn test_counter_canister() {
let pic = PocketIc::new();
// Create a canister and charge it with 2T cycles.
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, INIT_CYCLES);
// Install the counter canister wasm file on the canister.
let counter_wasm = std::fs::read("counter.wasm").unwrap();
pic.install_canister(canister_id, counter_wasm, vec![], None);
// Make some calls to the canister.
let reply = call_counter_can(&pic, canister_id, "read");
assert_eq!(reply, vec![0, 0, 0, 0]);
let reply = call_counter_can(&pic, canister_id, "write");
assert_eq!(reply, vec![1, 0, 0, 0]);
let reply = call_counter_can(&pic, canister_id, "write");
assert_eq!(reply, vec![2, 0, 0, 0]);
let reply = call_counter_can(&pic, canister_id, "read");
assert_eq!(reply, vec![2, 0, 0, 0]);
}
fn call_counter_can(pic: &PocketIc, canister_id: Principal, method: &str) -> Vec<u8> {
pic.update_call(
canister_id,
Principal::anonymous(),
method,
encode_one(()).unwrap(),
)
.expect("Failed to call counter canister")
}
Core Features
Canister Management
Create and Install Canisters
use pocket_ic::PocketIc;
let pic = PocketIc::new();
// Create a new canister
let canister_id = pic.create_canister();
// Add cycles
pic.add_cycles(canister_id, 2_000_000_000_000);
// Install WASM
let wasm = std::fs::read("my_canister.wasm").unwrap();
let init_args = vec![]; // Your init args here
pic.install_canister(canister_id, wasm, init_args, None);
Upgrade Canisters
let new_wasm = std::fs::read("my_canister_v2.wasm").unwrap();
let upgrade_args = vec![];
pic.upgrade_canister(canister_id, new_wasm, upgrade_args, None)
.expect("Upgrade failed");
Making Calls
Update Calls
use candid::{encode_args, decode_one};
// Make an update call
let result = pic.update_call(
canister_id,
Principal::anonymous(),
"my_method",
encode_args((arg1, arg2)).unwrap(),
)
.expect("Call failed");
// Decode the response
let response: MyType = decode_one(&result).unwrap();
Query Calls
// Make a query call (read-only, faster)
let result = pic.query_call(
canister_id,
Principal::anonymous(),
"get_value",
encode_args(()).unwrap(),
)
.expect("Query failed");
Time Control
use std::time::Duration;
// Get current time
let now = pic.get_time();
// Advance time by 1 hour
pic.advance_time(Duration::from_secs(3600));
// Set specific time
use std::time::SystemTime;
pic.set_time(SystemTime::now());
Subnet Management
use pocket_ic::{PocketIcBuilder, common::rest::SubnetKind};
// Create instance with multiple subnets
let pic = PocketIcBuilder::new()
.with_application_subnet()
.with_system_subnet()
.with_nns_subnet()
.build();
Testing Workflows
Integration Testing
Structure your tests to test canister interactions:
#[cfg(test)]
mod tests {
use super::*;
use pocket_ic::PocketIc;
fn setup() -> (PocketIc, Principal) {
let pic = PocketIc::new();
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 2_000_000_000_000);
let wasm = std::fs::read("target/wasm32-unknown-unknown/release/my_canister.wasm")
.expect("Wasm file not found");
pic.install_canister(canister_id, wasm, vec![], None);
(pic, canister_id)
}
#[test]
fn test_initialization() {
let (pic, canister_id) = setup();
// Test initialization logic
}
#[test]
fn test_state_changes() {
let (pic, canister_id) = setup();
// Test state modifications
}
}
Multi-Canister Testing
Test interactions between multiple canisters:
#[test]
fn test_canister_to_canister_calls() {
let pic = PocketIc::new();
// Deploy first canister
let canister_a = pic.create_canister();
pic.add_cycles(canister_a, 2_000_000_000_000);
let wasm_a = std::fs::read("canister_a.wasm").unwrap();
pic.install_canister(canister_a, wasm_a, vec![], None);
// Deploy second canister
let canister_b = pic.create_canister();
pic.add_cycles(canister_b, 2_000_000_000_000);
let wasm_b = std::fs::read("canister_b.wasm").unwrap();
pic.install_canister(canister_b, wasm_b, vec![], None);
// Test inter-canister call
let result = pic.update_call(
canister_a,
Principal::anonymous(),
"call_canister_b",
encode_args((canister_b,)).unwrap(),
)
.expect("Inter-canister call failed");
}
Testing Upgrades
#[test]
fn test_stable_storage_persists() {
let pic = PocketIc::new();
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 2_000_000_000_000);
// Install v1
let wasm_v1 = std::fs::read("canister_v1.wasm").unwrap();
pic.install_canister(canister_id, wasm_v1, vec![], None);
// Set some state
pic.update_call(
canister_id,
Principal::anonymous(),
"set_value",
encode_args((42u64,)).unwrap(),
).unwrap();
// Upgrade to v2
let wasm_v2 = std::fs::read("canister_v2.wasm").unwrap();
pic.upgrade_canister(canister_id, wasm_v2, vec![], None).unwrap();
// Verify state persisted
let result = pic.query_call(
canister_id,
Principal::anonymous(),
"get_value",
encode_args(()).unwrap(),
).unwrap();
let value: u64 = decode_one(&result).unwrap();
assert_eq!(value, 42);
}
Advanced Features
Custom Initialization
use pocket_ic::PocketIcBuilder;
let pic = PocketIcBuilder::new()
.with_server_binary("/custom/path/pocket-ic")
.with_application_subnet()
.build();
Tick Control
// Manually trigger IC rounds
pic.tick();
// Run multiple rounds
for _ in 0..10 {
pic.tick();
}
Root Key Access
// Get the root subnet's public key (for testing)
let root_key = pic.root_key();
Best Practices
Create a fresh PocketIc instance for each test to ensure isolation
Use sufficient cycles (at least 2T) for canister operations
Clean up test artifacts (WASM files) in your .gitignore
Use setup() functions to reduce test boilerplate
Test both success and error cases
Test canister upgrades to ensure stable storage works correctly
Do not share PocketIC instances between test cases, as this can lead to unexpected state pollution and flaky tests.
Examples and Resources
Minimal Example
Check out the ICP Hello World Rust repository for a minimal PocketIC setup.
Integration Tests
See PocketIC integration tests for simple but complete examples.
Large Test Suites
Explore the OpenChat integration tests for a production-scale test suite using PocketIC.
The OpenChat test suite shares instances between tests for performance, which is not recommended for most use cases.
Troubleshooting
Server Binary Not Found
If you see errors about the PocketIC server:
- Ensure the server binary is downloaded
- Set
POCKET_IC_BIN environment variable
- Or use
PocketIcBuilder::with_server_binary()
- Verify the binary is executable (Unix:
chmod +x pocket-ic)
Version Compatibility
Ensure your PocketIC library version matches a compatible server version. Check the compatibility matrix.
Out of Cycles
If calls fail with out-of-cycles errors, increase the cycles:
// Use more cycles for complex operations
pic.add_cycles(canister_id, 10_000_000_000_000); // 10T cycles
Learn More