PocketIC
Overview
PocketIC is a lightweight, deterministic testing solution for programmatic testing of canisters.
It seamlessly integrates with existing testing infrastructure, such as cargo test
, and runs as a standalone binary that doesn't require additional containers or virtual machines.
Currently, PocketIC is supported on macOS and Linux host systems.
While there are other options for testing canisters, they may come with pitfalls such as:
Installing and testing canisters on the mainnet provides the 'real' workflow experience, but will cost developers real cycles.
Testing using the local replica provided by
dfx
allows for testing on a single BIG node, but there is no cross-net or multi-subnet functionality. Testing with the local replica is not deterministic, and it is rather heavyweight. Additionally, testing withdfx
can be complex, slow, and require additional boilerplate code to get working.
PocketIC is designed to remedy these pitfalls, resulting in a testing environment that provides the following benefits:
Deterministic: Synchronous control over the local canister execution environment. PocketIC removes non-deterministic parts of the replica to make tests fully reproducible.
Lightweight: Only provides the necessary components, and strips away the consensus and networking layers.
Concurrent and independent BIG instances: Enabling tests to run in parallel
Multi-language support: Currently supports Rust, Python and JavaScript/TypeScript, but supports integration with any language that is written against the PocketIC REST-API.
Versatile: Allows for fine-grained control over the canister by providing the ability to set stable memory, control how time passes, and other testing environment variables.
Support for Xnet calls and multiple subnets.
While PocketIC can support integration with any language, Rust and Python will be covered in this guide.
PocketIC Rust
To use PocketIC, the latest PocketIC server binary must be downloaded from the PocketIC repo. You can download the latest version for MacOS or Linux systems here.
PocketIC is currently not natively supported on Windows systems.
Once the binary file is downloaded, unzip the file and make it executable:
gzip -d pocket-ic.gz
chmod +x pocket-ic
On macOS systems, to bypass developer verification from Apple, you may need to run:
xattr -dr com.apple.quarantine pocket-ic
The PocketIC binary can be left in your working directory or you can specify the path to the binary by setting the POCKET_IC_BIN
environmental variable.
Add PocketIC Rust do your project with the command:
cargo add pocket-ic --dev
PocketIC can be imported into your project with the code:
use pocket_ic::PocketIc;
Then, in your code you can create a new PocketIC instance with the code:
let pic = PocketIc::new();
Example: Single canister testing on a single subnet
The following is a simple, but complete testing example with a counter canister:
use candid::encode_one;
use pocket_ic::PocketIc;
#[test]
fn test_counter_canister() {
let pic = PocketIc::new();
// Create an empty canister as the anonymous principal and add cycles.
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 2_000_000_000_000);
let wasm_bytes = load_counter_wasm(...);
pic.install_canister(canister_id, wasm_bytes, vec![], None);
// 'inc' is a counter canister method.
call_counter_canister(&pic, canister_id, "inc");
// Check if it had the desired effect.
let reply = call_counter_canister(&pic, canister_id, "read");
assert_eq!(reply, WasmResult::Reply(vec![0, 0, 0, 1]));
}
fn call_counter_canister(pic: &PocketIc, canister_id: CanisterId, method: &str) -> WasmResult {
pic.update_call(canister_id, Principal::anonymous(), method, encode_one(()).unwrap())
.expect("Failed to call counter canister")
}
Example: Multi-subnet testing
Versions of PocketIC v2.0.0
and newer support multi-subnet testing. Multi-subnet testing allows for simulating multiple, different types of subnets locally. For example, to create an BIG instance with an NNS subnet and two application subnets, use the code:
let pic = PocketIcBuilder::new()
.with_nns_subnet()
.with_application_subnet()
.with_application_subnet()
.build();
Then, to target the NNS subnet to create a canister, use the code:
let nns_sub = pic.topology().get_nns_subnet().unwrap();
let nns_can_id = pic.create_canister_on_subnet(..., nns_sub);
To test one of the application subnets and install a canister, use the code:
let app_sub_2 = pic.topology().get_app_subnets()[1];
let app_can_id = pic.create_canister_on_subnet(..., app_sub_2);
pic.install_canister(app_can_id, ...);
To create a canister with a specific canister_id
on a named subnet, in this example the NNS subnet, use the code:
let ledger_canister_id = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
pic.create_canister_with_id(..., ledger_canister_id).unwrap();
pic.install_canister(ledger_canister_id, ...);
Possible types of subnets include:
Generic system subnets.
Generic application subnets.
Named subnets with canister ID ranges like on mainnet, such as the NNS, SPS, II, Bitcoin, and Fiduciary subnets.
For a larger, more complex example that uses cross canister calls on two different subnets, see the full code here.
PocketIC Python
To use PocketIC, the latest PocketIC server binary must be downloaded from the PocketIC repo. You can download the latest version for MacOS or Linux systems here.
PocketIC is currently not supported natively on Windows systems.
Once the binary file is downloaded, unzip the file and make it executable:
gzip -d pocket-ic.gz
chmod +x pocket-ic
On macOS systems, to bypass developer verification from Apple, you may need to run:
xattr -dr com.apple.quarantine pocket-ic
The PocketIC binary can be left in your working directory or you can specify the path to the binary by setting the POCKET_IC_BIN
environmental variable.
Then, run the following command to install PocketIC:
pip3 install pocket-ic
To import PocketIC into your Python code, use the line:
from pocket_ic import PocketIC
Example: create a canister with cycles
from pocket_ic import PocketIC
pic = PocketIC()
canister_id = pic.create_canister()
pic.add_cycles(canister_id, 2_000_000_000_000)
Make canister calls
response = pic.update_call(canister_id, method="greeting", ...)
assert(response == 'Hello, PocketIC!')
Make a call directly with a canister object
my_canister = pic.create_and_install_canister_with_candid(...)
Call your canister methods using the native Python syntax
response = my_canister.greeting()
assert(response == 'Hello, PocketIC!')
Example: Single canister testing on a single subnet
The following is a simple, but complete testing example with a counter canister:
import sys
import os
import unittest
import ic
from pocket_ic import PocketIC
class CounterCanisterTests(unittest.TestCase):
def test_counter_canister(self):
pic = PocketIC()
canister_id = pic.create_canister()
pic.add_cycles(canister_id, 1_000_000_000_000_000_000)
with open("counter.wasm", "rb") as wasm_file:
wasm_module = wasm_file.read()
pic.install_code(canister_id, bytes(wasm_module), [])
self.assertEqual(
pic.update_call(canister_id, "read", ic.encode([])),
[0, 0, 0, 0],
)
self.assertEqual(
pic.update_call(canister_id, "write", ic.encode([])),
[1, 0, 0, 0],
)
self.assertEqual(
pic.update_call(canister_id, "write", ic.encode([])),
[2, 0, 0, 0],
)
self.assertEqual(
pic.update_call(canister_id, "read", ic.encode([])),
[2, 0, 0, 0],
)
if __name__ == "__main__":
unittest.main()
To see more examples and run them locally, clone the PocketIC Python repo, then run the following command, replacing the example name with the example you'd like to run. For example, to run the counter_canister
example, use the command:
python3 examples/counter_canister/counter_canister_test.py