From f10264fff8b21c36a55915a7bd74fe6670ffd240 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 15:20:35 +0200 Subject: [PATCH 1/3] docs: PocketIC testing guide --- docs/guides/testing/pocket-ic.md | 387 ++++++++++++++++++++++++++++++- 1 file changed, 377 insertions(+), 10 deletions(-) diff --git a/docs/guides/testing/pocket-ic.md b/docs/guides/testing/pocket-ic.md index fa61f7c6..a2359044 100644 --- a/docs/guides/testing/pocket-ic.md +++ b/docs/guides/testing/pocket-ic.md @@ -5,16 +5,383 @@ sidebar: order: 2 --- -TODO: Write content for this page. +PocketIC is a lightweight, deterministic testing library for canister integration tests. Unlike the full local network started by `icp network start`, PocketIC runs entirely inside your test process — no daemon, no ports, no Docker required. Tests execute synchronously, making them fast and fully reproducible. - -Run integration tests using PocketIC, a lightweight IC replica for testing. Cover the Rust PocketIC library for Rust tests, Pic JS for JavaScript/TypeScript tests, multi-subnet testing, time travel (advancing time), and canister lifecycle in tests. Show setup, a basic test, and common patterns (deploy, call, assert). +The `icp-cli` local development network also uses PocketIC under the hood, so behavior you observe in tests closely matches what you see during development. - -- JS SDK: pic-js (https://js.icp.build/pic-js) -- icp-cli: guides/containerized-networks.md (Docker-based PocketIC) +**When to use PocketIC:** Use it for integration tests that need to deploy one or more canisters and make calls between them. For unit tests that test individual functions without deploying, use Rust's built-in test framework directly. See [Testing strategies](strategies.md) for guidance on when each approach fits. - -- guides/testing/strategies -- when to use PocketIC vs unit tests -- guides/governance/testing -- SNS testflight with PocketIC -- languages/rust/testing -- Rust-specific testing patterns +## How PocketIC works + +A PocketIC instance is an in-process IC replica. It supports: + +- Creating and installing canisters (from compiled `.wasm` files) +- Making update and query calls +- Multiple subnets (NNS, application, system) +- Time control — advance the clock without waiting +- Deterministic execution — the same test always produces the same result +- Parallel execution — each test gets its own `PocketIc` instance + +PocketIC strips the consensus and networking layers from the IC replica, keeping only the execution environment. This makes it orders of magnitude faster than running a full local network. + +## Client libraries + +PocketIC has client libraries for several languages: + +| Language | Package | Use case | +|----------|---------|----------| +| Rust | [`pocket-ic`](https://crates.io/crates/pocket-ic) | Rust canister tests | +| JavaScript/TypeScript | [`@hadronous/pic`](https://www.npmjs.com/package/@hadronous/pic) | Frontend and JS canister tests | +| Python | [`pocket-ic`](https://pypi.org/project/pocket-ic/) | Python-based tests | + +This guide covers Rust (the most common choice for backend canister tests) and JavaScript with Pic JS. + +## Rust: getting started + +### Add the dependency + +Add `pocket-ic` to your `Cargo.toml` as a dev dependency: + +```toml +[dev-dependencies] +pocket-ic = "*" +candid = "*" +``` + +### Write a basic test + +A typical PocketIC Rust test follows this pattern: create an instance, deploy a canister, make calls, assert results. + +```rust title=tests/integration_tests.rs +use candid::{decode_one, encode_one, Principal}; +use pocket_ic::PocketIc; + +// Path to the compiled canister WASM +pub const CANISTER_WASM: &[u8] = + include_bytes!("../target/wasm32-unknown-unknown/release/my_canister.wasm"); + +#[test] +fn test_counter() { + // Create a new PocketIC instance with one application subnet + let pic = PocketIc::new(); + + // Create a canister and fund it with 2T cycles + let canister_id = pic.create_canister(); + pic.add_cycles(canister_id, 2_000_000_000_000); + + // Install the canister WASM + pic.install_canister(canister_id, CANISTER_WASM.to_vec(), vec![], None); + + // Make a query call + let result = pic + .query_call( + canister_id, + Principal::anonymous(), + "get_count", + encode_one(()).unwrap(), + ) + .expect("query failed"); + + let count: u64 = decode_one(&result).unwrap(); + assert_eq!(count, 0); + + // Make an update call + pic.update_call( + canister_id, + Principal::anonymous(), + "increment", + encode_one(()).unwrap(), + ) + .expect("update failed"); + + // Verify the counter incremented + let result = pic + .query_call( + canister_id, + Principal::anonymous(), + "get_count", + encode_one(()).unwrap(), + ) + .expect("query failed"); + + let count: u64 = decode_one(&result).unwrap(); + assert_eq!(count, 1); +} +``` + +### Run the tests + +Build the canister WASM first, then run the tests: + +```bash +cargo build --target wasm32-unknown-unknown --release +cargo test +``` + +PocketIC automatically downloads the PocketIC server binary on first use and caches it in `~/.cache/pocket-ic/`. The `POCKET_IC_BIN` environment variable overrides the download path if you need a specific version. + +### Use a helper struct for cleaner tests + +For multiple tests against the same canister, extract setup into a helper struct: + +```rust title=tests/integration_tests.rs +use candid::{Decode, Encode, Principal}; +use pocket_ic::{PocketIc, WasmResult}; + +pub const CANISTER_WASM: &[u8] = + include_bytes!("../target/wasm32-unknown-unknown/release/my_canister.wasm"); + +pub struct CanisterFixture { + pub env: PocketIc, + pub canister_id: Principal, +} + +impl CanisterFixture { + pub fn new() -> Self { + let env = PocketIc::new(); + let canister_id = env.create_canister(); + env.add_cycles(canister_id, 2_000_000_000_000); + env.install_canister(canister_id, CANISTER_WASM.to_vec(), vec![], None); + Self { env, canister_id } + } + + pub fn query serde::Deserialize<'de>>( + &self, + method: &str, + args: Vec, + ) -> T { + match self + .env + .query_call(self.canister_id, Principal::anonymous(), method, args) + .expect("query failed") + { + WasmResult::Reply(bytes) => Decode!(&bytes, T).unwrap(), + WasmResult::Reject(e) => panic!("query rejected: {}", e), + } + } + + pub fn update serde::Deserialize<'de>>( + &self, + method: &str, + args: Vec, + ) -> T { + match self + .env + .update_call(self.canister_id, Principal::anonymous(), method, args) + .expect("update failed") + { + WasmResult::Reply(bytes) => Decode!(&bytes, T).unwrap(), + WasmResult::Reject(e) => panic!("update rejected: {}", e), + } + } +} + +#[test] +fn test_with_fixture() { + let canister = CanisterFixture::new(); + let count: u64 = canister.query("get_count", Encode!().unwrap()); + assert_eq!(count, 0); +} +``` + +### Canister lifecycle in tests + +PocketIC exposes the full canister lifecycle: + +```rust title=tests/lifecycle.rs +use pocket_ic::PocketIc; + +#[test] +fn test_upgrade() { + let pic = PocketIc::new(); + let canister_id = pic.create_canister(); + pic.add_cycles(canister_id, 2_000_000_000_000); + + // Install initial version + pic.install_canister(canister_id, WASM_V1.to_vec(), vec![], None); + + // Upgrade to new version + pic.upgrade_canister(canister_id, WASM_V2.to_vec(), vec![], None) + .expect("upgrade failed"); + + // Stop and start + pic.stop_canister(canister_id, None).unwrap(); + pic.start_canister(canister_id, None).unwrap(); +} +``` + +### Advance time + +Canisters that depend on the current time (for example, timers or time-locked state) can be tested by controlling the clock: + +```rust title=tests/timer.rs +use pocket_ic::PocketIc; +use std::time::Duration; + +#[test] +fn test_timer_fires() { + let pic = PocketIc::new(); + let canister_id = pic.create_canister(); + pic.add_cycles(canister_id, 2_000_000_000_000); + pic.install_canister(canister_id, CANISTER_WASM.to_vec(), vec![], None); + + // Advance the clock by 10 seconds and process any pending timers + pic.advance_time(Duration::from_secs(10)); + pic.tick(); // process one round of messages + + // Verify timer-triggered state change + // ... +} +``` + +`pic.tick()` processes one round of messages without advancing time. Call it after `advance_time` to execute any timers that have fired. + +### Multi-subnet testing + +Test canister interactions that span subnets — for example, cross-subnet calls or NNS integration: + +```rust title=tests/multi_subnet.rs +use pocket_ic::{PocketIc, PocketIcBuilder}; +use candid::Principal; + +#[test] +fn test_cross_subnet_call() { + // Build an instance with an NNS subnet and two application subnets + let pic = PocketIcBuilder::new() + .with_nns_subnet() + .with_application_subnet() + .with_application_subnet() + .build(); + + // Get subnet IDs from the topology + let app_subnets = pic.topology().get_app_subnets(); + let subnet_a = app_subnets[0]; + let subnet_b = app_subnets[1]; + + // Create canisters on specific subnets + let canister_a = pic.create_canister_on_subnet(None, None, subnet_a); + pic.add_cycles(canister_a, 2_000_000_000_000); + + let canister_b = pic.create_canister_on_subnet(None, None, subnet_b); + pic.add_cycles(canister_b, 2_000_000_000_000); + + // Install and test cross-subnet interactions + // ... +} +``` + +Named subnets (NNS, SNS, II) carry the same canister ID ranges as mainnet, which matters when testing code that references specific canister IDs. + +## JavaScript/TypeScript: Pic JS + +Pic JS (`@hadronous/pic`) is the JavaScript/TypeScript client for PocketIC, designed for testing frontend code, agent-based workflows, or JavaScript canister backends. It exposes the same PocketIC capabilities with a Promise-based API. + +### Install + +```bash +npm install --save-dev @hadronous/pic +``` + +Pic JS downloads the PocketIC server binary on first use. Set `POCKET_IC_URL` to point to a running PocketIC server if you prefer managing it yourself. + +### Write a basic test + +This example uses [Jest](https://jestjs.io/), but Pic JS works with any test runner. + +```typescript title=src/__tests__/counter.test.ts +import { PocketIc, createIdentity } from '@hadronous/pic'; +import { resolve } from 'node:path'; + +const WASM_PATH = resolve(__dirname, '../../target/wasm32-unknown-unknown/release/counter.wasm'); + +describe('Counter canister', () => { + let pic: PocketIc; + + beforeEach(async () => { + pic = await PocketIc.create(); + }); + + afterEach(async () => { + await pic.tearDown(); + }); + + it('should increment and read the counter', async () => { + // Set up a fixture with the canister + const fixture = await pic.setupCanister({ + wasm: WASM_PATH, + }); + + const { actor } = fixture; + + // Call methods on the canister via the generated actor + await actor.increment(); + const count = await actor.get_count(); + expect(count).toBe(1n); + }); +}); +``` + +Pic JS generates typed actors from Candid declarations automatically when you use `setupCanister`. See the [Pic JS documentation](https://js.icp.build/pic-js) for the full API, including typed actor generation and subnet configuration. + +### Advance time in JavaScript tests + +```typescript title=src/__tests__/timer.test.ts +import { PocketIc } from '@hadronous/pic'; + +it('should trigger timer after delay', async () => { + const pic = await PocketIc.create(); + // ... deploy canister ... + + // Advance time by 10 seconds and tick + await pic.advanceTime(10_000); // milliseconds + await pic.tick(); + + // Assert timer-triggered state change + // ... + + await pic.tearDown(); +}); +``` + +## Running PocketIC tests in CI + +PocketIC downloads its server binary on first use and caches it. In CI environments, cache this directory to avoid repeated downloads: + +```yaml title=.github/workflows/test.yml +- name: Cache PocketIC binary + uses: actions/cache@v4 + with: + path: ~/.cache/pocket-ic + key: pocket-ic-${{ runner.os }} + +- name: Run integration tests + run: | + cargo build --target wasm32-unknown-unknown --release + cargo test +``` + +PocketIC runs on macOS and Linux. Windows is not currently supported for standalone PocketIC use, but the containerized network (`icp network start`) supports Windows. + +## Connecting to a running network for testing + +For end-to-end tests that need a full network with all system canisters, use a containerized network instead of PocketIC. See the [icp-cli containerized networks documentation](https://cli.internetcomputer.org/) for how to configure Docker-based test networks in `icp.yaml`. + +The containerized network is appropriate when: + +- You need Internet Identity or NNS canisters pre-installed +- You are testing frontend interactions via HTTP +- You need to test with real cycle mechanics + +PocketIC is appropriate when: + +- You are testing canister logic in isolation +- You want fast, parallelizable tests without Docker +- You need deterministic time control or multi-subnet simulation + +## Next steps + +- [Testing strategies](strategies.md) — overview of unit, integration, and end-to-end testing +- [Governance testing](../governance/testing.md) — SNS testflight with PocketIC +- [Rust testing patterns](../../languages/rust/testing.md) — Rust-specific patterns including unit testing with mocks + + From 1b41e50a130406e7b5e8d63ee9ad45ba5384f079 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 15:50:29 +0200 Subject: [PATCH 2/3] fix: update Pic JS to @dfinity/pic with current PocketIcServer API Co-Authored-By: Claude Sonnet 4.6 --- docs/guides/testing/pocket-ic.md | 33 ++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/guides/testing/pocket-ic.md b/docs/guides/testing/pocket-ic.md index a2359044..7965c394 100644 --- a/docs/guides/testing/pocket-ic.md +++ b/docs/guides/testing/pocket-ic.md @@ -31,7 +31,7 @@ PocketIC has client libraries for several languages: | Language | Package | Use case | |----------|---------|----------| | Rust | [`pocket-ic`](https://crates.io/crates/pocket-ic) | Rust canister tests | -| JavaScript/TypeScript | [`@hadronous/pic`](https://www.npmjs.com/package/@hadronous/pic) | Frontend and JS canister tests | +| JavaScript/TypeScript | [`@dfinity/pic`](https://www.npmjs.com/package/@dfinity/pic) | Frontend and JS canister tests | | Python | [`pocket-ic`](https://pypi.org/project/pocket-ic/) | Python-based tests | This guide covers Rust (the most common choice for backend canister tests) and JavaScript with Pic JS. @@ -274,31 +274,40 @@ Named subnets (NNS, SNS, II) carry the same canister ID ranges as mainnet, which ## JavaScript/TypeScript: Pic JS -Pic JS (`@hadronous/pic`) is the JavaScript/TypeScript client for PocketIC, designed for testing frontend code, agent-based workflows, or JavaScript canister backends. It exposes the same PocketIC capabilities with a Promise-based API. +Pic JS (`@dfinity/pic`) is the JavaScript/TypeScript client for PocketIC, designed for testing frontend code, agent-based workflows, or JavaScript canister backends. It exposes the same PocketIC capabilities with a Promise-based API. ### Install ```bash -npm install --save-dev @hadronous/pic +npm install --save-dev @dfinity/pic ``` -Pic JS downloads the PocketIC server binary on first use. Set `POCKET_IC_URL` to point to a running PocketIC server if you prefer managing it yourself. +Pic JS manages the PocketIC server process for you via `PocketIcServer`. Set `POCKET_IC_SERVER_PATH` to point to a pre-installed binary if you prefer not to download it automatically. ### Write a basic test -This example uses [Jest](https://jestjs.io/), but Pic JS works with any test runner. +This example uses [Jest](https://jestjs.io/), but Pic JS works with Vitest, Bun, and any other Node-compatible test runner. ```typescript title=src/__tests__/counter.test.ts -import { PocketIc, createIdentity } from '@hadronous/pic'; +import { PocketIc, PocketIcServer } from '@dfinity/pic'; import { resolve } from 'node:path'; const WASM_PATH = resolve(__dirname, '../../target/wasm32-unknown-unknown/release/counter.wasm'); describe('Counter canister', () => { + let picServer: PocketIcServer; let pic: PocketIc; + beforeAll(async () => { + picServer = await PocketIcServer.start(); + }); + + afterAll(async () => { + await picServer.stop(); + }); + beforeEach(async () => { - pic = await PocketIc.create(); + pic = await PocketIc.create(picServer.getUrl()); }); afterEach(async () => { @@ -306,14 +315,12 @@ describe('Counter canister', () => { }); it('should increment and read the counter', async () => { - // Set up a fixture with the canister const fixture = await pic.setupCanister({ wasm: WASM_PATH, }); const { actor } = fixture; - // Call methods on the canister via the generated actor await actor.increment(); const count = await actor.get_count(); expect(count).toBe(1n); @@ -326,10 +333,11 @@ Pic JS generates typed actors from Candid declarations automatically when you us ### Advance time in JavaScript tests ```typescript title=src/__tests__/timer.test.ts -import { PocketIc } from '@hadronous/pic'; +import { PocketIc, PocketIcServer } from '@dfinity/pic'; it('should trigger timer after delay', async () => { - const pic = await PocketIc.create(); + const picServer = await PocketIcServer.start(); + const pic = await PocketIc.create(picServer.getUrl()); // ... deploy canister ... // Advance time by 10 seconds and tick @@ -340,6 +348,7 @@ it('should trigger timer after delay', async () => { // ... await pic.tearDown(); + await picServer.stop(); }); ``` @@ -384,4 +393,4 @@ PocketIC is appropriate when: - [Governance testing](../governance/testing.md) — SNS testflight with PocketIC - [Rust testing patterns](../../languages/rust/testing.md) — Rust-specific patterns including unit testing with mocks - + From d4481f574421e8adca861f62939101cb25b06baf Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 17:09:30 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix(pocket-ic):=20address=20review=20feedba?= =?UTF-8?q?ck=20=E2=80=94=20API=20consistency,=20idlFactory,=20version=20p?= =?UTF-8?q?in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guides/testing/pocket-ic.md | 40 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/docs/guides/testing/pocket-ic.md b/docs/guides/testing/pocket-ic.md index 7965c394..03eca403 100644 --- a/docs/guides/testing/pocket-ic.md +++ b/docs/guides/testing/pocket-ic.md @@ -44,7 +44,7 @@ Add `pocket-ic` to your `Cargo.toml` as a dev dependency: ```toml [dev-dependencies] -pocket-ic = "*" +pocket-ic = "9" candid = "*" ``` @@ -125,8 +125,8 @@ PocketIC automatically downloads the PocketIC server binary on first use and cac For multiple tests against the same canister, extract setup into a helper struct: ```rust title=tests/integration_tests.rs -use candid::{Decode, Encode, Principal}; -use pocket_ic::{PocketIc, WasmResult}; +use candid::{decode_one, encode_one, Encode, Principal}; +use pocket_ic::PocketIc; pub const CANISTER_WASM: &[u8] = include_bytes!("../target/wasm32-unknown-unknown/release/my_canister.wasm"); @@ -150,14 +150,11 @@ impl CanisterFixture { method: &str, args: Vec, ) -> T { - match self + let bytes = self .env .query_call(self.canister_id, Principal::anonymous(), method, args) - .expect("query failed") - { - WasmResult::Reply(bytes) => Decode!(&bytes, T).unwrap(), - WasmResult::Reject(e) => panic!("query rejected: {}", e), - } + .expect("query failed"); + decode_one(&bytes).unwrap() } pub fn update serde::Deserialize<'de>>( @@ -165,14 +162,11 @@ impl CanisterFixture { method: &str, args: Vec, ) -> T { - match self + let bytes = self .env .update_call(self.canister_id, Principal::anonymous(), method, args) - .expect("update failed") - { - WasmResult::Reply(bytes) => Decode!(&bytes, T).unwrap(), - WasmResult::Reject(e) => panic!("update rejected: {}", e), - } + .expect("update failed"); + decode_one(&bytes).unwrap() } } @@ -191,6 +185,10 @@ PocketIC exposes the full canister lifecycle: ```rust title=tests/lifecycle.rs use pocket_ic::PocketIc; +// WASM_V1 and WASM_V2 are defined like CANISTER_WASM above, pointing to +// different compiled versions of the same canister +// e.g.: pub const WASM_V1: &[u8] = include_bytes!("../target/.../my_canister_v1.wasm"); + #[test] fn test_upgrade() { let pic = PocketIc::new(); @@ -282,7 +280,7 @@ Pic JS (`@dfinity/pic`) is the JavaScript/TypeScript client for PocketIC, design npm install --save-dev @dfinity/pic ``` -Pic JS manages the PocketIC server process for you via `PocketIcServer`. Set `POCKET_IC_SERVER_PATH` to point to a pre-installed binary if you prefer not to download it automatically. +Pic JS manages the PocketIC server process for you via `PocketIcServer`. ### Write a basic test @@ -291,6 +289,9 @@ This example uses [Jest](https://jestjs.io/), but Pic JS works with Vitest, Bun, ```typescript title=src/__tests__/counter.test.ts import { PocketIc, PocketIcServer } from '@dfinity/pic'; import { resolve } from 'node:path'; +// idlFactory is generated from the canister's Candid interface (e.g. via icp-cli or candid-extractor) +// _SERVICE is the TypeScript type for the canister's public API +import { idlFactory, type _SERVICE } from '../declarations/counter'; const WASM_PATH = resolve(__dirname, '../../target/wasm32-unknown-unknown/release/counter.wasm'); @@ -315,7 +316,8 @@ describe('Counter canister', () => { }); it('should increment and read the counter', async () => { - const fixture = await pic.setupCanister({ + const fixture = await pic.setupCanister<_SERVICE>({ + idlFactory, wasm: WASM_PATH, }); @@ -328,10 +330,12 @@ describe('Counter canister', () => { }); ``` -Pic JS generates typed actors from Candid declarations automatically when you use `setupCanister`. See the [Pic JS documentation](https://js.icp.build/pic-js) for the full API, including typed actor generation and subnet configuration. +Pic JS generates typed actors from Candid declarations automatically when you use `setupCanister`. The `idlFactory` is generated from your canister's `.did` file by `icp-cli` — it lives in the `declarations/` directory alongside the TypeScript types. See the [Pic JS documentation](https://js.icp.build/pic-js) for the full API, including typed actor generation and subnet configuration. ### Advance time in JavaScript tests +This example uses inline setup for brevity. For test suites with multiple tests, the `beforeAll`/`afterAll` pattern from the basic example above is preferred — it avoids restarting the server for each test. + ```typescript title=src/__tests__/timer.test.ts import { PocketIc, PocketIcServer } from '@dfinity/pic';