Testing smart contract with gtest library
In this part of our course, you'll learn how to test smart contracts in the Rust programming language using the Gear gtest
library.
Here's what we'll cover:
- Creating a test file
- Defining a test function
- Initializing the environment to run the program
- Sending messages to the program
- Checking test results
Testing smart contracts is crucial when developing decentralized applications. We'll use the Gear gtest
library for our program's logic testing.
Let's start by creating a new directory called tests
at the top level of our project directory, next to the src
directory.
We'll create the hello_world_test.rs
file to write tests for our contract in the directory.
mkdir tests
cd tests
touch hello_world_test.rs
In our test file, we'll import the necessary types from the gtest
library, such as Log
, Program
and System
.
We'll also define a test function:
use gtest::{Log, Program, System};
#[test]
fn hello_test() {}
Before testing our smart contract, we'll create an environment for running programs using the System
structure from gtest
. The System
emulates the node behavior:
let sys = System::new();
Next, we'll create a mockup of our program using the Program
structure from gtest
. There are two ways to create a program mockup:
- From a file by its path
- By pointing to the program itself (current program)
To create a program mockup from a Wasm file:
let program = Program::from_file(&sys,
"./target/wasm32-unknown-unknown/release/hello_world.wasm");
To create a program mockup from the program itself:
let program = Program::current(&sys);
The uploaded program has its own id. You can specify the program id manually using the Program::from_file_with_id
constructor.
If you don't specify the program id, the id of the first initialized program will be 0x010000…00
(32-byte one, LSB first), and the next program initialized without an id specification will have an id of 0x020000…00
(32-byte two, LSB first) and so on.
In the next step, we'll send messages to our program.
- To send a message to the program, call one of two
Program
methods:send
orsend_bytes
. The difference between them is similar togstd
functionsmsg::send
andmsg::send_bytes
. - The first argument in these functions is a sender id, and the second is a message payload.
- You can specify the sender id as a hex, byte array (
[u8; 32]
), string oru64
. However, you can't send a message from the id already taken by the program! - The first message sent to the
Program
structure is always the initialization message, regardless of whether the program has aninit
function or not. In our case, we can use any message. However, let's include theinit
function in our program and observe if that message reaches the program.
#![no_std]
use gstd::{msg, prelude::*, debug};
#[no_mangle]
extern "C" fn handle() {
msg::reply(String::from("Hello"), 0)
.expect("Error in sending a reply message");
}
#[no_mangle]
extern "C" fn init() {
let init_message: String = msg::load()
.expect("Can't load init message");
debug!("Program was initialized with message {:?}",
init_message);
}
In our test function, we can send a message to the program using the Program::send
function:
#[test]
fn hello_test() {
let sys = System::new();
sys.init_logger();
let program = Program::current(&sys);
program.send(2, String::from("INIT MESSAGE"));
}
We added sys.init_logger()
to initialize printing logs into stdout and sent a message from the user with id 2 (id 2 transforms to ActorId
equal to 0x020000…00
).
We can then run our test using cargo test
:
cargo test --release
If everything is working correctly, we should see the debug message in our console:
[DEBUG hello_test] Program was initialized with message "INIT MESSAGE"
test hello_test ... ok
Sending functions in the gtest
library will return RunResult
structure. It contains the final result of the processing message and other messages created during the execution.
For example, we can check the init message processing result by ensuring the log is empty and the program doesn't reply or send any messages.
To do this, we can use the assert!(res.log().is_empty())
command.
-
Contains empty log (the program doesn't reply and does not send any messages):
assert!(res.log().is_empty());
-
Was successful:
assert!(!res.main_failed());
After confirming the successful initialization message, we process the next messages through the handle
function. To test this, we can send the next message using the program.send(2, String::from("Hello"))
command.
let res = program.send(2, String::from("Hello"));
Here, we'll confirm if the program replied with the expected hello message. We can accomplish this by utilizing the Log
structure from the gtest
library and constructing the anticipated log.
To create the expected log, we'll use the command Log::builder().dest(2).payload(String::from("Hello"))
.
After creating the expected log, we can then check if the received log contains the expected log. We'll use the assert!(res.contains(&expected_log))
command.
let expected_log = Log::builder()
.dest(2)
.payload(String::from("Hello"));
assert!(res.contains(&expected_log));
In this case:
dest
represents the account where the program sends a messagepayload
contains the content of the message
Run the test to ensure everything works correctly.