Expand description
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:
- Initialize the
System
structure. - Initialize the
Program
structure. - 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 viagtest
is 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 smart contracts.
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:
cargo test
gtest
capabilities
Let’s take a closer look at the gtest
capabilities.
Initialization of the network environment for running smart contracts
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 from a Wasm-file with a custom id using the
Program::from_file_with_id
function:let prog = Program::from_file_with_id( &sys, 105, "./target/wasm32-unknown-unknown/release/demo_ping.wasm", );
Every place in this lib, where you need to specify some ids, it requires generic type
ID
, which implementsInto<ProgramIdWrapper>
.ProgramIdWrapper
may be built from:u64
[u8; 32]
String
&str
ProgramId
(fromgear_core
one’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 prog = sys.get_program(105);
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
Sending messages
To send message to the program need to call one of two program’s functions:
Program::send
(orProgram::send_with_value
if you need to send a message with attached funds).Program::send_bytes
(orProgram::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 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 Log
s 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::InactiveProgram);
// 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);
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.
// 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:
// 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);
Re-exports
pub use codec;
Modules
- Module containing constants of Gear protocol.
Macros
- Construct state arguments.
- Construct encoded state arguments.
Structs
- A log that emitted by a program, for user defined logs, see
Log
. - Gas for gear programs.
- A log that can be emitted by a program.
- Gear program instance.
- The result of a message run.
- The testing environment which simulates the chain state and its transactions but somehow the real on-chain execution environment could be different.
Enums
- List of general errors.
Traits
- Trait for mocking gear programs.
Functions
- Calculate program id from code id and salt.
Type Aliases
- Type alias for the testing functions running result.