IC-Mockery: Mocking Framework for IC Canisters in Rust

IC-Mockery is a testing and mocking framework built for Internet Computer (IC) canister development in Rust.

It simplifies mocking of async method calls by transforming them into HTTP outcalls—enabling full test coverage with PocketIC while keeping production code untouched.

:sparkles: Features

  • Mock Async HTTP Calls: Seamlessly mock any async fn -> Result<_, _> method.
  • Built on PocketIC: Simulate canister behavior locally.
  • Proc Macro Powered: Use #[mock_async_calls] to transform methods.
  • Fluent API: Easy-to-read mocks and call chains.
    Flexible Error Handling: Works with any error type that implements From<String>, including custom error enums and (RejectionCode, String).

:package: Installation

# In Cargo.toml
[dependencies]
ic-mockery = { git = "https://github.com/ic-mockery/ic-mockery" }
ic-mockery-macro = { git = "https://github.com/ic-mockery/ic-mockery" }

:test_tube: Usage Example

A full example using #[mock_async_calls] and AsyncMocker.

1. Define Types

use candid::{CandidType, Deserialize};
use serde::{Serialize};

#[derive(CandidType, Deserialize, Serialize, Clone)]
pub struct GreetRequest {
    pub name: String,
}

#[derive(CandidType, Deserialize, Serialize, Debug, PartialEq)]
pub struct GreetResponse {
    pub message: String,
    pub status: Status,
}

#[derive(CandidType, Deserialize, Serialize, Debug, PartialEq)]
pub enum Status {
    Success,
    Error,
}

2. Implement Your Canister Logic

Use the #[mock_async_calls] macro to make your service methods mockable.

use ic_mockery_macro::mock_async_calls;

pub struct HelloService;

#[mock_async_calls]
impl HelloService {
    pub async fn greet(req: GreetRequest) -> Result<GreetResponse, String> {
        // This method will be transformed to use HTTP outcalls under the hood
        Ok(GreetResponse {
            message: format!("Hello, {}!", req.name),
            status: Status::Success,
        })
    }

    pub async fn prepare_greet(req: GreetRequest) -> Result<(), String> {
        // Also mockable
        Ok(())
    }
}

3. Expose Canister API

Use the service from your actual canister entrypoint.

#[ic_cdk::update]
async fn greet(req: GreetRequest) -> GreetResponse {
    HelloService::prepare_greet(req.clone()).await.unwrap();
    HelloService::greet(req).await.unwrap()
}

5. Test Cases

Mock in action

#[test]
fn test_greet_functionality() {
    // Create a PocketIC test environment
    let pic = PocketIcBuilder::new().with_application_subnet().build();
    let canister = pic.create_canister();

    // Add cycles and install the canister (replace `wasm_bytes` with actual WASM)
    pic.add_cycles(canister, 2_000_000_000_000);
    pic.install_canister(canister, wasm_bytes, vec![], None);

    // Set up mocks and run the test
    let result = AsyncMocker::new(&pic)
        .call(
            canister,
            Principal::anonymous(),
            "greet",
            GreetRequest {
                name: "Wizard".into(),
            },
        )
        .mock("prepare_greet", |_| to_value(()).unwrap())
        .mock("greet", |args| {
            let name = args["args"][0]["name"].as_str().unwrap();
            to_value(GreetResponse {
                message: format!("Hello, {}!", name),
                status: Status::Success,
            }).unwrap()
        })
        .execute::<GreetResponse>()
        .unwrap();

    assert_eq!(result.message, "Hello, Wizard!");
    assert_eq!(result.status, Status::Success);
}

Failure simulation

#[test]
fn test_greet_failure() {
    // Step 1: Set up a local PocketIC environment
    let pic = PocketIcBuilder::new().with_application_subnet().build();
    let canister = pic.create_canister();

    // Step 2: Fund and install the canister (replace `wasm_bytes` with your compiled WASM)
    pic.add_cycles(canister, 2_000_000_000_000);
    pic.install_canister(canister, wasm_bytes, vec![], None);

    // Step 3: Call the `greet` method, mocking internal calls
    let result = AsyncMocker::new(&pic)
        .call(
            canister,
            Principal::anonymous(),
            "greet",
            GreetRequest {
                name: "Error".into(), // This input is irelevant since we are simulating failure anyway
            },
        )
        // Mock `prepare_greet` to succeed (returns `Ok(())`)
        .mock("prepare_greet", |_| to_value(()).unwrap())
        // Mock `greet` to fail with a specific error
        .mock_fail("greet", "Invalid name provided")
        .execute::<GreetResponse>();

    // Step 4: Verify that the failure is properly handled
    assert!(result.is_err());
    assert_eq!(result.unwrap_err(), "Invalid name provided");
}

:white_check_mark: Final Thoughts

The goal of IC-Mockery is to make testing async IC canisters simple, safe, and fully mockable with minimal boilerplate, full type safety.

github.com/ic-mockery/ic-mockery — feedback & PRs welcome!

8 Likes

thanks for creating and sharing this @pxr64 :slight_smile:

I think there is a small little typo :wink:

1 Like