Rust测试框架
原文:https://docs.elrond.com/developers/developer-reference/rust-testing-framework
Rust 测试框架是作为手动编写Mandos测试的替代方案而开发的。这带来了许多好处:
- 能够使用变量计算值
- 类型检查
- 自动序列化
- 不那么啰嗦
- Mandos测试的半自动生成
唯一的缺点是你需要学习新的东西!玩笑归玩笑,记住这整个框架是在一个被嘲笑的环境中运行的。因此,虽然您获得了强大的测试和调试工具,但您最终运行的是一个模拟,并且无法保证该合约将与 mainnet 上部署的当前 VM 版本一致地工作。
这就是Mandos一代发挥作用的地方。Rust 测试框架允许您以最少的努力生成Mandos场景,然后通过我们的Elrond VSCode 扩展(或者,只需运行erdpy contract test
)单击运行所述场景。开发者需要做一些手工工作,但是我们将在具体的章节中讨论。
请注意,mandos 生成更多的是一个实验,而不是一个完全成熟的实现,我们甚至可能在未来删除它。不过,如果您仍然希望尝试,这里提供了一些示例。
先决条件
你需要有最新的 elrond-wasm 版本(在写这篇文章的时候,最新版本是 0.31.1)。你可以在这里查看最新版本:https://crates.io/crates/elrond-wasm
将elrond-wasm-debug
和所需的包作为开发依赖项添加到 Cargo.toml:
[dev-dependencies.elrond-wasm-debug]
version = "0.31.1"
[dev-dependencies]
num-bigint = "0.4.2"
num-traits = "0.2"
hex = "0.4"
对于本教程,我们将使用众筹 SC,因此让它打开或克隆存储库可能会很方便:https://github . com/ElrondNetwork/elrond-wasm-RS/tree/master/contracts/examples/crowd funding-esdt
在你的合约中需要一个tests
和一个mandos
文件夹。在你的tests
文件夹中创建一个.rs
文件。
在您新创建的测试文件中,添加以下代码(根据您的合约修改crowdfunding_esdt
名称空间、结构/变量名和合约 wasm 路径):
use crowdfunding_esdt::*;
use elrond_wasm::{
sc_error,
types::{Address, SCResult},
};
use elrond_wasm_debug::{
managed_address, managed_biguint, managed_token_id, rust_biguint, testing_framework::*,
DebugApi,
};
const WASM_PATH: &'static str = "crowdfunding-esdt/output/crowdfunding-esdt.wasm";
struct CrowdfundingSetup<CrowdfundingObjBuilder>
where
CrowdfundingObjBuilder:
'static + Copy + Fn() -> crowdfunding_esdt::ContractObj<DebugApi>,
{
pub blockchain_wrapper: BlockchainStateWrapper,
pub owner_address: Address,
pub first_user_address: Address,
pub second_user_address: Address,
pub cf_wrapper:
ContractObjWrapper<crowdfunding_esdt::ContractObj<DebugApi>, CrowdfundingObjBuilder>,
}
并不真的需要这个CrowdfundingSetup
结构,但是它有助于消除一些代码的重复。如果需要的话,你可以在你的结构中添加其他的字段,但是现在这对我们的用例来说已经足够了。任何合约都只需要blockchain_wrapper
和cf_wrapper
字段。其余的字段可以根据您的测试场景进行调整。
这就是你开始工作所需要的一切。
写作你的第一次测试
您需要编写的第一个测试是模拟您的智能合约的部署。为此,您需要一个用户地址和一个合约地址。然后你简单的调用智能合约的init
函数。
因为我们将在任何地方使用相同的代币 ID,所以让我们将它作为一个常量添加(同时,让截止日期也作为一个常量):
const CF_TOKEN_ID: &[u8] = b"CROWD-123456";
const CF_DEADLINE: u64 = 7 * 24 * 60 * 60; // 1 week in seconds
让我们创建初始设置:
fn setup_crowdfunding<CrowdfundingObjBuilder>(
cf_builder: CrowdfundingObjBuilder,
) -> CrowdfundingSetup<CrowdfundingObjBuilder>
where
CrowdfundingObjBuilder: 'static + Copy + Fn() -> crowdfunding_esdt::ContractObj<DebugApi>,
{
let rust_zero = rust_biguint!(0u64);
let mut blockchain_wrapper = BlockchainStateWrapper::new();
let owner_address = blockchain_wrapper.create_user_account(&rust_zero);
let first_user_address = blockchain_wrapper.create_user_account(&rust_zero);
let second_user_address = blockchain_wrapper.create_user_account(&rust_zero);
let cf_wrapper = blockchain_wrapper.create_sc_account(
&rust_zero,
Some(&owner_address),
cf_builder,
WASM_PATH,
);
blockchain_wrapper.set_esdt_balance(&first_user_address, CF_TOKEN_ID, &rust_biguint!(1_000));
blockchain_wrapper.set_esdt_balance(&second_user_address, CF_TOKEN_ID, &rust_biguint!(1_000));
blockchain_wrapper
.execute_tx(&owner_address, &cf_wrapper, &rust_zero, |sc| {
let target = managed_biguint!(2_000);
let token_id = managed_token_id!(CF_TOKEN_ID);
sc.init(target, CF_DEADLINE, token_id);
})
.assert_ok();
blockchain_wrapper.add_mandos_set_account(cf_wrapper.address_ref());
CrowdfundingSetup {
blockchain_wrapper,
owner_address,
first_user_address,
second_user_address,
cf_wrapper,
}
}
您将要与之交互的主要对象是BlockchainStateWrapper
。它在任何给定的时刻保存着整个(被嘲笑的)区块链状态,并允许你与账户进行交互。
正如您在上面的测试中看到的,我们使用上述包装器创建了一个所有者帐户、两个其他用户帐户和众筹智能合约帐户。
然后,我们通过使用BlockchainStateWrapper
对象的execute_tx
函数,为两个用户设置 ESDT 余额,并部署智能合约。这些论点是:
- 呼叫者地址
- 合约包装器(包含合约地址和合约对象构建器)
- EGLD 付款金额
- 一个 lambda 函数,包含实际执行
因为这是一个 SC 部署,所以我们调用init
函数。由于合约对托管对象起作用,所以我们不能使用内置的 Rust BigUint,而是使用elrond_wasm
提供的那个。为了创建托管类型,我们使用了managed_
函数。或者,您可以通过以下方式创建这些对象:
let target = BigUint::<DebugApi>::from(2_000u32);
请记住,您不能在execute_tx
函数之外创建托管类型。
对execute_tx
功能的一些观察:
- lambda 函数的返回类型是一个
TxResult
,它有检查成功或错误的方法:assert_ok()
用于检查 tx 是否工作。如果你想检查错误案例,你可以使用assert_user_error("message")
。 - 在运行了
init
函数之后,我们在生成的Mandos中添加了一个setState
步骤,以模拟我们的部署:blockchain_wrapper.add_mandos_set_account(cf_wrapper.address_ref());
要测试场景并生成Mandos文件,您必须创建一个测试函数:
#[test]
fn init_test() {
let cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
cf_setup
.blockchain_wrapper
.write_mandos_output("_generated_init.scen.json");
}
这一步就完成了。您成功地测试了合约的 init 函数,并为它生成了一个Mandos场景。
测试交易
让我们测试一下fund
函数。为此,我们将使用之前的设置,但现在我们使用execute_esdt_transfer
方法而不是execute_tx
,因为我们在调用fund
时将 ESDT 发送到合约:
#[test]
fn fund_test() {
let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
let b_wrapper = &mut cf_setup.blockchain_wrapper;
let user_addr = &cf_setup.first_user_address;
b_wrapper
.execute_esdt_transfer(
user_addr,
&cf_setup.cf_wrapper,
CF_TOKEN_ID,
0,
&rust_biguint!(1_000),
|sc| {
sc.fund();
let user_deposit = sc.deposit(&managed_address!(user_addr)).get();
let expected_deposit = managed_biguint!(1_000);
assert_eq!(user_deposit, expected_deposit);
},
)
.assert_ok();
}
如您所见,我们可以从合约中直接调用存储映射器(如deposit
)并与本地值进行比较。不需要编码任何东西。
如果您还想为该交易生成一个Mandos场景文件,这就是手工工作的用武之地:
let mut sc_call = ScCallMandos::new(user_addr, cf_setup.cf_wrapper.address_ref(), "fund");
sc_call.add_esdt_transfer(CF_TOKEN_ID, 0, &rust_biguint!(1_000));
let expect = TxExpectMandos::new(0);
b_wrapper.add_mandos_sc_call(sc_call, Some(expect));
cf_setup
.blockchain_wrapper
.write_mandos_output("_generated_fund.scen.json");
你必须在你的fund_test
末尾加上这个。调用越复杂,你需要添加的参数就越多。SCCallMandos
结构有add_argument
方法,所以你不必自己做任何编码。
检测查询
测试查询类似于测试交易,只是参数更少(因为没有调用者,也没有付款,任何修改都会自动恢复):
#[test]
fn status_test() {
let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
let b_wrapper = &mut cf_setup.blockchain_wrapper;
b_wrapper
.execute_query(&cf_setup.cf_wrapper, |sc| {
let status = sc.status();
assert_eq!(status, Status::FundingPeriod);
})
.assert_ok();
let sc_query = ScQueryMandos::new(cf_setup.cf_wrapper.address_ref(), "status");
let mut expect = TxExpectMandos::new(0);
expect.add_out_value(&Status::FundingPeriod);
b_wrapper.add_mandos_sc_query(sc_query, Some(expect));
cf_setup
.blockchain_wrapper
.write_mandos_output("_generated_query_status.scen.json");
}
测试智能合约错误
在前面的交易测试中,我们已经测试了快乐流。现在让我们看看如何检查错误:
#[test]
fn test_sc_error() {
let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
let b_wrapper = &mut cf_setup.blockchain_wrapper;
let user_addr = &cf_setup.first_user_address;
b_wrapper.set_egld_balance(user_addr, &rust_biguint!(1_000));
b_wrapper
.execute_tx(
user_addr,
&cf_setup.cf_wrapper,
&rust_biguint!(1_000),
|sc| {
sc.fund();
},
)
.assert_user_error("wrong token");
b_wrapper
.execute_tx(user_addr, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
let user_deposit = sc.deposit(&managed_address!(user_addr)).get();
let expected_deposit = managed_biguint!(0);
assert_eq!(user_deposit, expected_deposit);
})
.assert_ok();
let mut sc_call = ScCallMandos::new(user_addr, cf_setup.cf_wrapper.address_ref(), "fund");
sc_call.add_egld_value(&rust_biguint!(1_000));
let mut expect = TxExpectMandos::new(4);
expect.set_message("wrong token");
b_wrapper.add_mandos_sc_call(sc_call, Some(expect));
cf_setup
.blockchain_wrapper
.write_mandos_output("_generated_sc_err.scen.json");
}
请注意我们是如何故意将付款更改为无效代币来检查错误情况的。此外,我们已将预期存款更改为“0”,而不是之前的“1_000”。最后:对结果的.assert_user_error("wrong token")
调用。
测试一次成功的资助活动
对于这个场景,我们需要两个用户都提供全额资金,然后所有者申请资金。为简单起见,我们将Mandos一代排除在外:
#[test]
fn test_successful_cf() {
let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
let b_wrapper = &mut cf_setup.blockchain_wrapper;
let owner = &cf_setup.owner_address;
let first_user = &cf_setup.first_user_address;
let second_user = &cf_setup.second_user_address;
// first user fund
b_wrapper
.execute_esdt_transfer(
first_user,
&cf_setup.cf_wrapper,
CF_TOKEN_ID,
0,
&rust_biguint!(1_000),
|sc| {
sc.fund();
let user_deposit = sc.deposit(&managed_address!(first_user)).get();
let expected_deposit = managed_biguint!(1_000);
assert_eq!(user_deposit, expected_deposit);
},
)
.assert_ok();
// second user fund
b_wrapper
.execute_esdt_transfer(
second_user,
&cf_setup.cf_wrapper,
CF_TOKEN_ID,
0,
&rust_biguint!(1_000),
|sc| {
sc.fund();
let user_deposit = sc.deposit(&managed_address!(second_user)).get();
let expected_deposit = managed_biguint!(1_000);
assert_eq!(user_deposit, expected_deposit);
},
)
.assert_ok();
// set block timestamp after deadline
b_wrapper.set_block_timestamp(CF_DEADLINE + 1);
// check status
b_wrapper
.execute_query(&cf_setup.cf_wrapper, |sc| {
let status = sc.status();
assert_eq!(status, Status::Successful);
})
.assert_ok();
// user try claim
b_wrapper
.execute_tx(first_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
sc.claim();
})
.assert_user_error("only owner can claim successful funding");
// owner claim
b_wrapper
.execute_tx(owner, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
sc.claim();
})
.assert_ok();
b_wrapper.check_esdt_balance(owner, CF_TOKEN_ID, &rust_biguint!(2_000));
b_wrapper.check_esdt_balance(first_user, CF_TOKEN_ID, &rust_biguint!(0));
b_wrapper.check_esdt_balance(second_user, CF_TOKEN_ID, &rust_biguint!(0));
}
您已经看到了这个测试中的大部分代码。唯一的新东西是包装器的set_block_timestamp
和check_esdt_balance
方法。有类似的方法来设置块随机数、块随机种子等。,以及检查 EGLD 和 SFT/NFT 余额。
测试一次失败的资助活动
这与前一个类似,但是我们让用户而不是所有者在截止日期后索赔。
#[test]
fn test_failed_cf() {
let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
let b_wrapper = &mut cf_setup.blockchain_wrapper;
let owner = &cf_setup.owner_address;
let first_user = &cf_setup.first_user_address;
let second_user = &cf_setup.second_user_address;
// first user fund
b_wrapper
.execute_esdt_transfer(
first_user,
&cf_setup.cf_wrapper,
CF_TOKEN_ID,
0,
&rust_biguint!(300),
|sc| {
sc.fund();
let user_deposit = sc.deposit(&managed_address!(first_user)).get();
let expected_deposit = managed_biguint!(300);
assert_eq!(user_deposit, expected_deposit);
},
)
.assert_ok();
// second user fund
b_wrapper
.execute_esdt_transfer(
second_user,
&cf_setup.cf_wrapper,
CF_TOKEN_ID,
0,
&rust_biguint!(600),
|sc| {
sc.fund();
let user_deposit = sc.deposit(&managed_address!(second_user)).get();
let expected_deposit = managed_biguint!(600);
assert_eq!(user_deposit, expected_deposit);
},
)
.assert_ok();
// set block timestamp after deadline
b_wrapper.set_block_timestamp(CF_DEADLINE + 1);
// check status
b_wrapper
.execute_query(&cf_setup.cf_wrapper, |sc| {
let status = sc.status();
assert_eq!(status, Status::Failed);
})
.assert_ok();
// first user claim
b_wrapper
.execute_tx(first_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
sc.claim();
})
.assert_ok();
// second user claim
b_wrapper
.execute_tx(second_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
sc.claim();
})
.assert_ok();
b_wrapper.check_esdt_balance(owner, CF_TOKEN_ID, &rust_biguint!(0));
b_wrapper.check_esdt_balance(first_user, CF_TOKEN_ID, &rust_biguint!(1_000));
b_wrapper.check_esdt_balance(second_user, CF_TOKEN_ID, &rust_biguint!(1_000));
}
结论
这项测试几乎涵盖了众筹智能合约中的每一个流程。请记住,通过使用类似于setup_crowdfunding
函数的函数,可以对代码进行更多的重复数据删除,但是出于示例的考虑,我们尽可能保持简单。我们希望这将使编写测试和调试更加容易!