跳转至

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_wrappercf_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_timestampcheck_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函数的函数,可以对代码进行更多的重复数据删除,但是出于示例的考虑,我们尽可能保持简单。我们希望这将使编写测试和调试更加容易!



回到顶部