ic-test 0.1.14

This tool helps to organize IC Rust canister testing as well as cross-testing between ICP and EVM.
Documentation

ic-test

ic-test is a command-line tool that helps to set up and manage Rust canister tests on the Internet Computer (IC) using. It makes it easier to create a test project and includes the basic files and setup needed for both IC canisters and optionally EVM (Ethereum Virtual Machine) smart contracts.

The tool reads the dfx.json (must exist) and the foundry.toml (may exist) files in order to build the test environment automatically. It uses pocket-ic and alloy (foundry) to run tests. The generated code and helpers provide:

  • A simple way to start a test project.
  • A single, easy-to-use interface for testing both IC and EVM parts.
  • Type checking and auto-completion support.
  • Easy functions for deploying and calling canisters or contracts.

Overview

ic-test will:

  • Read dfx.json to get canister details.
  • Read foundry.toml to get contract details.
  • Generate Rust types from Candid (.did) files.
  • Generate contract interfaces from Solidity (.sol) files.
  • Provide API to work with .wasm canisters and .json contract files in tests.

Requirements

  • Rust
  • DFX – to build and locally deploy canisters.
  • Foundry – optional, if you want to test EVM contract's interaction with canisters.

Installation

cargo install ic-test

Tool usage

ic-test <COMMAND> [OPTIONS]

Without arguments it starts in interactive mode to create a new test project. If an ic-test.json config file exists already, the "update" mode will regenerate the existing test project bindings.

Create a new test project

ic-test new tests
  • Creates a new test project in the tests folder.
  • Looks for canisters and contracts, generates API bindings and a sample test.
  • Generates an ic-test.json configuration file.
  • Fails if the tests folder already exists, the user would need to choose a different name.

Update/regenerate an existing test project

ic-test update

Regenerates bindings using the configuration in ic-test.json.

"Hello world" tutorial

Create a "Hello, World!" canister:

dfx new hello-ic-test --type rust --no-frontend

Compile the project:

dfx start --clean --background

dfx canister create --all

dfx build

Generate test bindings

If there are uncommitted changes, either commit them before generating or use the --force flag:

ic-test new tests --force

This creates a tests package with:

  • Canister API bindings in tests/src/bindings
  • Test environment setup logic in test_setup.rs
  • A test template in tests.rs

Example test

Edit tests.rs:

use ic_test::IcpTest;

use crate::test_setup;

#[tokio::test]
async fn test_greet() {
    let test_setup::Env {
        icp_test,
        hello_ic_test_backend,
    } = test_setup::setup(IcpTest::new().await).await;

    let result = hello_ic_test_backend
        .greet("ic-test".to_string())
        .call()
        .await;

    assert_eq!(result, "Hello, ic-test!");
}

Run tests:

cargo test

Adding a counter

Update the canister backend:

use std::cell::RefCell;

#[ic_cdk::query]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}

#[derive(Clone, Default)]
struct CounterState {
    value: u64,
    increment: u64,
}

thread_local! {
    static STATE: RefCell<CounterState> = RefCell::new(CounterState::default());
}

#[ic_cdk::init]
fn init(init_value: u64, increment: u64) {
    STATE.with(|state| {
        *state.borrow_mut() = CounterState {
            value: init_value,
            increment,
        };
    });
}

#[ic_cdk::update]
fn increment_counter() {
    STATE.with(|state| {
        let mut s = state.borrow_mut();
        s.value += s.increment;
    });
}

#[ic_cdk::query]
fn get_counter() -> u64 {
    STATE.with(|state| state.borrow().value)
}

Update Candid file hello-ic-test-backend.did:

service : (nat64, nat64) -> {
  "greet": (text) -> (text) query;
  "get_counter": () -> (nat64) query;
  "increment_counter": () -> ();
}

Set initialization arguments in dfx.json:

{
  "canisters": {
    "hello-ic-test-backend": {
      "candid": "src/hello-ic-test-backend/hello-ic-test-backend.did",
      "package": "hello-ic-test-backend",
      "type": "rust",
      "init_arg": "(50, 73)"
    }
  },
  "defaults": {
    "build": {
      "args": "",
      "packtool": ""
    }
  },
  "output_env_file": ".env",
  "version": 1
}

Regenerate the bindings:

dfx build

ic-test

The ic-test will enter interactive mode and prompt user to allow overwriting the test_setup.rs file. Upon confirmation the the test_setup.rs is regenerated with the initialization parameters:

//...
    let hello_ic_test_backend = hello_ic_test_backend::deploy(&icp_user, 50, 73)
        .call()
        .await;

// ...

New test

Add a new test in tests.rs:

#[tokio::test]
async fn test_counter() {
    let test_setup::Env {
        icp_test,
        hello_ic_test_backend,
    } = test_setup::setup(IcpTest::new().await).await;

    let result = hello_ic_test_backend.get_counter().call().await;

    assert_eq!(result, 50u64);

    hello_ic_test_backend.increment_counter().call().await;

    let result = hello_ic_test_backend.get_counter().call().await;

    assert_eq!(result, 123u64); // 50 + 73
}

Run tests:

cargo test

More examples

For other examples, see https://github.com/wasm-forge/ic-test-examples.

Documentation

See the documentation on doc.rs.

Licence

Licensed under MIT license.