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:
[]
= "my-gear-app"
= "0.1.0"
= ["Your Name"]
= "2021"
[]
= { = "https://github.com/gear-tech/gear.git", = "v1.0.1" }
[]
= { = "https://github.com/gear-tech/gear.git", = "v1.0.1" }
[]
= { = "https://github.com/gear-tech/gear.git", = "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:
- Initialize the
Systemstructure. - Initialize the
Programstructure. - Send an init message to the program. Even though we don't have the
initfunction in our program, the first message to the program sent viagtestis always the init one. - Send a handle message to the program.
- 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};
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);
// Send an init message to the program.
let res = prog.send_bytes(USER_ID, b"Doesn't matter");
// Check whether the program was initialized successfully.
assert!(!res.main_failed());
// Send a handle message to the program.
let res = prog.send_bytes(USER_ID, b"PING");
// 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!(!res.main_failed());
// 3. Make sure the log entry is in the result.
assert!(res.contains(&log));
}
}
To run the test, use the following command:
gtest capabilities
Let's take a closer look at the gtest capabilities.
Initialization of the network environment for running programs
# use gtest::System;
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:
# use gtest::Program;
# let sys = gtest::System::new();
let prog = Program::current(&sys);
- Initialize a program from a Wasm-file with a default id using the
[
Program::from_file] function:
# use gtest::Program;
# let sys = gtest::System::new();
let prog = Program::from_file(
&sys,
"./target/wasm32-unknown-unknown/release/demo_ping.wasm",
);
- Initialize a program via builder:
# use gtest::ProgramBuilder;
# let sys = gtest::System::new();
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&strProgramId(fromgear_coreone's, not fromgstd).
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 sys = gtest::System::new();
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:
# let sys = gtest::System::new();
sys.init_logger();
To specify printed logs, set the env variable RUST_LOG:
RUST_LOG="target_1=logging_level,target_2=logging_level"
Sending messages
To send message to the program need to call one of two program's functions:
- [
Program::send] (or [Program::send_with_value] if you need to send a message with attached funds). - [
Program::send_bytes] (or [Program::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.
[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 sys = gtest::System::new();
# let prog = gtest::Program::current(&sys);
let res = prog.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:
- [
RunResult::log] — returns the reference to the Vec produced to users messages. You may assert them as you wish, iterating through them. - [
RunResult::main_failed] — returns bool which shows that there was panic during the execution of the main message. - [
RunResult::others_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. - [
RunResult::contains] — returns bool which shows that logs contain a given log. Syntax sugar aroundres.log().iter().any(|v| v == arg).
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.
# use gtest::Log;
# use gear_core_errors::ErrorReplyReason;
// Constructor for success log.
let log = Log::builder();
// Constructor for error reply log.
let log = Log::error_builder(ErrorReplyReason::InactiveActor);
# let sys = gtest::System::new();
# let prog = gtest::Program::current(&sys);
// 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]>.
# use gtest::Log;
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);
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-based network is 3 sec duration.
# let sys = gtest::System::new();
// Spend 150 blocks (7.5 mins for 3 sec block).
sys.spend_blocks(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:
# use gtest::Program;
# let sys = gtest::System::new();
// 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 mut prog = Program::current(&sys);
prog.mint(1000);
assert_eq!(prog.balance(), 1000);