Skip to main content

How to test a program

The Gear gtest library is the recommended option for the programs (smart contracts) logic testing. This article describes how to test programs using gtest.

Basics

Gear uses the standard for Rust programs testing mechanism - test build mode from cargo.

In accordance with basic concepts and testing methods described in Rustbook, tests can be organized in two main categories: unit tests and integration tests.

The unit tests enable testing of each unit of code in isolation from the rest of the code. It helps to quickly find where the code works as expected and where not. The unit tests should be placed in the src directory in each file with the code that they test.

Even when units of code work correctly, it is important to test if several parts of the library work together correctly as well. For integration tests, a separate tests directory is required at the top level of your project directory, next to src. You can make as many test files in this directory as you need, Cargo will compile each of the files as an individual crate.

Building a program in test mode

First of all, make sure you have a compiled Wasm file of the program you want to test. You can refer to Getting Started for additional details.

  1. Usually the following command is used for regular compilation of Gear smart contracts:

    cd ~/gear/contracts/first-gear-app/
    cargo build --release

    Nightly compiler is required if some unstable features have been used:

    cargo +nightly build --release
  2. The minimal command for running tests is:

    cargo test

    Nightly compiler is required if your contract uses unstable Rust features and the compiler will ask you to enable nightly if necessary. Only if you are writing the tests as unit/integration tests, rather than providing a separate library containing only the tests.

    cargo +nightly test

    Build tests in release mode so they run faster:

    cargo test --release

Import gtest lib

In order to use the gtest library, it must be imported into your Cargo.toml file in the [dev-dependencies] block in order to fetch and compile it for tests only

[package]
name = "first-gear-app"
version = "0.1.0"
authors = ["Your Name"]
edition = "2021"

[dependencies]
gstd = { git = "https://github.com/gear-tech/gear.git", features = ["debug"] }

[build-dependencies]
gear-wasm-builder = { git = "https://github.com/gear-tech/gear.git" }

[dev-dependencies]
gtest = { git = "https://github.com/gear-tech/gear.git" }

gtest capabilities

  • Initialization of the common environment for running smart contracts:
    // This emulates node's and chain's behavior.
//
// By default, sets:
// - current block equals 0
// - current timestamp equals UNIX timestamp of your system.
// - minimal message id equal 0x010000..
// - minimal program id equal 0x010000..
let sys = System::new();
  • Program initialization:
    // Initialization of program structure from file.
//
// Takes as arguments reference to the related `System` and the path to wasm binary relatively
// the root of the crate where the test was written.
//
// Sets free program id from the related `System` to this program. For this case it equals 0x010000..
// Next program initialized without id specification will have id 0x020000.. and so on.
let _ = Program::from_file(
&sys,
"./target/wasm32-unknown-unknown/release/demo_ping.wasm",
);

// Also, you may use the `Program::current()` function to load the current program.
let _ = Program::current(&sys);

// We can check the id of the program by calling `id()` function.
//
// It returns `ProgramId` type value.
let ping_pong_id = ping_pong.id();

// There is also a `from_file_with_id` constructor to manually specify the id of the program.
//
// Every place in this lib, where you need to specify some ids,
// it requires generic type 'ID`, which implements `Into<ProgramIdWrapper>`.
//
// `ProgramIdWrapper` may be built from:
// - u64;
// - [u8; 32];
// - String;
// - &str;
// - ProgramId (from `gear_core` one's, not from `gstd`).
//
// String implementation means the input as hex (with or without "0x")

// Numeric
let _ = Program::from_file_with_id(
&sys,
105,
"./target/wasm32-unknown-unknown/release/demo_ping.wasm",
);

// Hex with "0x"
let _ = Program::from_file_with_id(
&sys,
"0xe659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e",
"./target/wasm32-unknown-unknown/release/demo_ping.wasm",
);

// Hex without "0x"
let _ = Program::from_file_with_id(
&sys,
"e659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df5e",
"./target/wasm32-unknown-unknown/release/demo_ping.wasm",
);

// Array [u8; 32] (e.g. filled with 5)
let _ = Program::from_file_with_id(
&sys,
[5; 32],
"./target/wasm32-unknown-unknown/release/demo_ping.wasm",
);

// If you initialize program not in this scope, in cycle, in other conditions,
// where you didn't save the structure, you may get the object from the system by id.
let _ = sys.get_program(105);
  • Getting the program from the system:
    // If you initialize program not in this scope, in cycle, in other conditions,
// where you didn't save the structure, you may get the object from the system by id.
let _ = sys.get_program(105);
  • Initialization of styled env_logger:
    // Initialization of styled `env_logger` to print logs (only from `gwasm` by default) into stdout.
//
// To specify printed logs, set the env variable `RUST_LOG`:
// `RUST_LOG="target_1=logging_level,target_2=logging_level" cargo test`
//
// Gear smart contracts use `gwasm` target with `debug` logging level
sys.init_logger();
  • Sending messages:
    To send message to the program need to call one of two program's functions:
// `send()` or `send_bytes()` (or `send_with_value` and `send_bytes_with_value` if you need to send a message with attached funds).
//
// Both of the methods require sender id as the first argument and the payload as second.
//
// The difference between them is pretty simple and similar to `gstd` functions
// `msg::send()` and `msg::send_bytes()`.
//
// The first one requires payload to be CODEC Encodable, while the second requires payload
// implement `AsRef<[u8]>`, that means to be able to represent as bytes.
//
// `send()` uses `send_bytes()` under the hood with bytes from payload.encode().
//
// First message to the initialized program structure is always the init message.
let res = program.send_bytes(100001, "INIT MESSAGE");
  • Processing the result of the program execution:
    // Any sending functions in the lib returns `RunResult` structure.
//
// It contains the final result of the processing message and others,
// which were created during the execution.
//
// It has 4 main functions.

// Returns the reference to the Vec produced to users messages.
// You may assert them as you wish, iterating through them.
assert!(res.log().is_empty());

// Returns bool which shows that there was panic during the execution
// of the main message.
assert!(!res.main_failed());

// Returns bool which shows that there was panic during the execution
// of the created messages during the main execution.
//
// Equals false if no others were called.
assert!(!res.others_failed());

// Returns bool which shows that logs contain a given log.
//
// Syntax sugar around `res.log().iter().any(|v| v == arg)`.
assert!(!res.contains(&Log::builder()));

// To build a log for assertion you need to use `Log` structure with its builders.
// All fields here are optional.
// Assertion with Logs from core are made on the Some(..) fields
// You will run into panic if you try to set the already specified field.
//
// Constructor for success log.
let _ = Log::builder();

// Constructor for error reply log.
//
// Note that error reply never contains payload.
// And its exit code equals 1, instead of 0 for success replies.
let _ = Log::error_builder();

// Let’s send a new message after the program has been initialized.
// The initialized program expects to receive a byte string "PING" and replies with a byte string "PONG".
let res = ping_pong.send_bytes(100001, "PING");

// Other fields are set optionally by `dest()`, `source()`, `payload()`, `payload_bytes()`.
//
// The logic for `payload()` and `payload_bytes()` is the same as for `send()` and `send_bytes()`.
// First requires an encodable struct. The second requires bytes.
let log = Log::builder()
.source(ping_pong_id)
.dest(100001)
.payload_bytes("PONG");

assert!(res.contains(&log));

let wrong_log = Log::builder().source(100001);

assert!(!res.contains(&wrong_log));

// Log also has `From` implementations from (ID, T) and from (ID, ID, T),
// where ID: Into<ProgramIdWrapper>, T: AsRef<[u8]>
let x = Log::builder().dest(5).payload_bytes("A");
let x_from: Log = (5, "A").into();

assert_eq!(x, x_from);

let y = Log::builder().dest(5).source(15).payload_bytes("A");
let y_from: Log = (15, 5, "A").into();

assert_eq!(y, y_from);

assert!(!res.contains(&(ping_pong_id, ping_pong_id, "PONG")));
assert!(res.contains(&(1, 100001, "PONG")));
  • Spending blocks:
    // You may control time in the system by spending blocks.
//
// It adds the amount of blocks passed as arguments to the current block of the system.
// Same for the timestamp. Note, that for now 1 block in Gear network is 1 sec duration.
sys.spend_blocks(150);
  • Reading the program state:
    // To read the program state you need to call one of two program's functions:
// `meta_state()` or `meta_state_with_bytes()`.
//
// The methods require the payload as the input argument.
//
// The first one requires payload to be CODEC Encodable, while the second requires payload
// implement `AsRef<[u8]>`, that means to be able to represent as bytes.
//
// Let we have the following contract state and `meta_state` function:
#[derive(Encode, Decode, TypeInfo)]
pub struct ContractState {
a: u128,
b: u128,
}

pub enum State {
A,
B,
}

pub enum StateReply {
A(u128),
B(u128),
}

#[no_mangle]
pub unsafe extern "C" fn meta_state() -> *mut [i32; 2] {
let query: State = msg::load().expect("Unable to decode `State`");
let encoded = match query {
State::A => StateReply::A(STATE.a),
State::B => StateReply::B(STATE.b),
}.encode();
gstd::util::to_leak_ptr(encoded)
}

// Let's send a query from gtest:
let reply: StateReply = self
.meta_state(&State::A)
.expect("Meta_state failed");
let expected_reply = StateReply::A(10);
assert_eq!(reply,expected_reply);

// If your `meta_state` function doesn't require input payloads,
// you can use `meta_state_empty` or `meta_state_empty_with_bytes` functions
// without any arguments.
  • Balance:
    // If you need to send a message with value you have to mint balance for the message sender:
let user_id = 42;
sys.mint_to(user_id, 5000);
assert_eq!(sys.balance_of(user_id), 5000);

// To give the balance to the program you should use `mint` method:
let prog = Program::current(&sys);
prog.mint(1000);
assert_eq!(prog.balance(), 1000);