Skip to main content

PocketIC

Intermediate
Tutorial

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 IC 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 with dfx 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 IC 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 IC 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, SNS, 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

Resources