23 releases (stable)

1.6.2 Oct 7, 2024
1.5.0 Aug 12, 2024
1.4.2 Jul 2, 2024
1.1.1 Feb 12, 2024
0.0.1 Jul 6, 2021

#32 in #gear

Download history 256/week @ 2024-07-29 171/week @ 2024-08-05 403/week @ 2024-08-12 232/week @ 2024-08-19 216/week @ 2024-08-26 223/week @ 2024-09-02 112/week @ 2024-09-09 234/week @ 2024-09-16 764/week @ 2024-09-23 275/week @ 2024-09-30 362/week @ 2024-10-07 218/week @ 2024-10-14 372/week @ 2024-10-21 180/week @ 2024-10-28 387/week @ 2024-11-04 99/week @ 2024-11-11

1,046 downloads per month
Used in 3 crates

GPL-3.0 license

1.5MB
27K SLoC

Testing with gtest

gtest simulates a real network by providing mockups of the user, program, balances, mailbox, etc. Since it does not include parts of the actual blockchain, it is fast and lightweight. But being a model of the blockchain network, gtest cannot be a complete reflection of the latter.

As we said earlier, gtest is excellent for unit and integration testing. It is also helpful for debugging Gear program logic. Nothing other than the Rust compiler is required for running tests based on gtest. It is predictable and robust when used in continuous integration.

Main concepts

gtest is a library that provides a set of tools for testing Gear programs. The most important structures are:

  • System — a structure that represents the environment of the Gear network. It contains the current block number, timestamp, and other parameters. It also stores the mailbox and the list of programs.
  • Program — a structure that represents a Gear program. It contains the information about program and allows sending messages to other programs.
  • [Log] — a structure that represents a message log. It allows checking the result of the program execution.

Let's take a closer look at how to write tests using gtest.

Import gtest lib

To use the gtest library, you must import it into your Cargo.toml file in the [dev-dependencies] block to fetch and compile it for tests only:

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

[dependencies]
gstd = { git = "https://github.com/gear-tech/gear.git", tag = "v1.0.1" }

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

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

Program example

Let's write a simple program that will receive a message and reply to it.

lib.rs:

#![no_std]
use gstd::msg;

#[no_mangle]
extern "C" fn handle() {
    let payload = msg::load_bytes().expect("Failed to load payload");

    if payload == b"PING" {
        msg::reply_bytes(b"PONG", 0).expect("Failed to send reply");
    }
}

build.rs:

fn main() {
    gear_wasm_builder::build();
}

We will add a test that will check the program's behavior. To do this, we will use the gtest library.

Our test will consist of the following steps:

  1. Initialize the System structure.
  2. Initialize the Program structure.
  3. Send an init message to the program. Even though we don't have the init function in our program, the first message to the program sent via gtest is always the init one.
  4. Send a handle message to the program.
  5. Check the result of the program execution.

Add these lines to the bottom of the lib.rs file:

#[cfg(test)]
mod tests {
    use gtest::{Log, Program, System};

    // Alternatively, you can use the default users from `gtest::constants`:
    // `DEFAULT_USER_ALICE`, `DEFAULT_USER_BOB`, `DEFAULT_USER_CHARLIE`, `DEFAULT_USER_EVE`.
    // The full list of default users can be obtained with `gtest::constants::default_users_list`.
    const USER_ID: u64 = 100001;

    #[test]
    fn test_ping_pong() {
        // Initialization of the common environment for running programs.
        let sys = System::new();

        // Initialization of the current program structure.
        let prog = Program::current(&sys);

        // Provide user with some balance.
        sys.mint_to(USER_ID, EXISTENTIAL_DEPOSIT * 1000);

        // Send an init message to the program.
        let init_message_id = prog.send_bytes(USER_ID, b"Doesn't matter");

        // Run execution of the block which will contain `init_message_id`
        let block_run_result = sys.run_next_block();

        // Check whether the program was initialized successfully.
        assert!(block_run_result.succeed.contains(&init_message_id));

        // Send a handle message to the program.
        let handle_message_id = prog.send_bytes(USER_ID, b"PING");
        let block_run_result = sys.run_next_block();

        // Check the result of the program execution.
        // 1. Create a log pattern with the expected result.
        let log = Log::builder()
            .source(prog.id())
            .dest(USER_ID)
            .payload_bytes(b"PONG");

        // 2. Check whether the program was executed successfully.
        assert!(block_run_result.succeed.contains(&handle_message_id));

        // 3. Make sure the log entry is in the result.
        assert!(block_run_result.contains(&log));
    }
}

To run the test, use the following command:

cargo test

gtest capabilities

Let's take a closer look at the gtest capabilities.

Initialization of the network environment for running programs

let sys = System::new();

This emulates node's and chain's behavior. By default, the System::new function sets the following parameters:

  • current block equals 0
  • current timestamp equals UNIX timestamp of your system
  • starting message id equals 0x010000..
  • starting program id equals 0x010000..

Program initialization

There are a few ways to initialize a program:

  • Initialize the current program using the Program::current function:

    let prog = Program::current(&sys);
    
  • Initialize a program from a Wasm-file with a default id using the Program::from_file function:

    let prog = Program::from_file(
        &sys,
        "./target/wasm32-unknown-unknown/release/demo_ping.wasm",
    );
    
  • Initialize a program via builder:

    let prog = ProgramBuilder::from_file("your_gear_program.wasm")
        .with_id(105)
        .build(&sys);
    

    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").

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 prog = sys.get_program(105).unwrap();

Initialization of styled env_logger

Initialization of styled env_logger to print logs (only from gwasm by default) into stdout:

sys.init_logger();

To specify printed logs, set the env variable RUST_LOG:

RUST_LOG="target_1=logging_level,target_2=logging_level" cargo test

Pre-requisites for sending a message

Prior to sending a message, it is necessary to mint sufficient balance for the sender to ensure coverage of the existential deposit and gas costs.

let user_id = 42;
sys.mint_to(user_id, EXISTENTIAL_DEPOSIT * 1000);

Alternatively, you can use the default users from gtest::constants, which have a preallocated balance, as the message sender.

assert_eq!(
    sys.balance_of(gtest::constants::DEFAULT_USER_ALICE),
    DEFAULT_USERS_INITIAL_BALANCE
);

Sending messages

To send message to the program need to call one of two program's functions:

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.

Program::send uses Program::send_bytes under the hood with bytes from payload.encode().

First message to the initialized program structure is always the init message.

let res = prog.send_bytes(100001, "INIT MESSAGE");

Processing the result of the program execution

Any sending functions in the lib returns an id of the sent message.

In order to actually get the result of the program execution the block execution should be triggered (see "Block execution model" section). Block execution function returns the result of the block run (BlockRunResult)

It contains the final result of the processing message and others, which were created during the execution.

It has 2 main functions:

  • BlockRunResult::log — returns the reference to the Vec produced to users messages. You may assert them as you wish, iterating through them.
  • BlockRunResult::contains — returns bool which shows that logs contain a given log. Syntax sugar around res.log().iter().any(|v| v == arg).

Fields of the type are public, and some of them can be really useful:

  • field succeed is a set of ids of messages that were successfully executed.
  • field failed is a set of ids of messages that failed during the execution.

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 = Log::builder();

// Constructor for error reply log.
let log = Log::error_builder(ErrorReplyReason::InactiveActor);
// Other fields are set optionally by `dest()`, `source()`, `payload()`, `payload_bytes()`.
let log = Log::builder()
    .source(prog.id())
    .dest(100001)
    .payload_bytes("PONG");

Log also has From implementations from (ID, T) and from (ID_1, ID_2, 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);

Blocks execution model

Block execution has 2 main step:

  • tasks processing
  • messages processing

Tasks processing is a step, when all scheduled for the current block number tasks are tried to be processed. This includes processing delayed dispatches, waking waited messages and etc.

Messages processing is a step, when messages from the queue are processed until either the queue is empty or the block gas allowance is not enough for the execution.

Blocks can't be "spent" without their execution except for use the System::run_scheduled_tasks method, which doesn't process the message queue, but only processes scheduled tasks triggering blocks info adjustments, which can be used to "spend" blocks.

Note, that for now 1 block in Gear-based network is 3 sec duration.

// Spend 150 blocks by running only the task pool (7.5 mins for 3 sec block).
sys.run_scheduled_tasks(150);

Note that processing messages (e.g. by using Program::send/Program::send_bytes methods) doesn't spend blocks, nor changes the timestamp. If you write time dependent logic, you should spend blocks manually.

Balance

There are certain invariants in gtest regarding user and program balances:

  • For a user to successfully send a message to the program, they must have sufficient balance to cover the existential deposit and gas costs.
  • The program charges the existential deposit from the user upon receiving the initial message.

As previously mentioned here, a balance for the user must be minted before sending a message. This balance should be sufficient to cover the following: the user's existential deposit, the existential deposit of the initialized program (the first message to the program charges the program's existential deposit from the sender), and the message's gas costs.

The System::mint_to method can be utilized to allocate a balance to the user or the program. The System::balance_of method may be used to verify the current 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 [`System::transfer`] method:
let mut prog = Program::current(&sys);
sys.transfer(user_id, prog.id(), 1000, true);
assert_eq!(prog.balance(), 1000);

Dependencies

~71MB
~1.5M SLoC