13 个版本 (4 个稳定版)
4.0.0-pre.9 | 2022年5月12日 |
---|---|
4.0.0-pre.7 | 2022年2月3日 |
4.0.0-pre.5 | 2021年12月24日 |
4.0.0-pre.4 | 2021年10月15日 |
3.0.1 | 2021年3月30日 |
#91 in #near
612 每月下载量
用于 near-accounts-plugins-wra…
1MB
20K SLoC
NEAR 模拟器 & 跨合约测试库
在编写 NEAR 合同时,使用 Rust 或其他 Wasm 编译语言(如 AssemblyScript),您选择的语言(例如在您的 Rust 项目的 src/lib.rs
文件中的 mod test
)的默认测试方法非常适合测试单个合同在隔离状态下的行为。
但是,区块链和智能合约的真正力量来自跨合约调用。您如何确保您的跨合约代码按预期工作呢?
作为第一步,您可以使用这个库!通过它,您可以
要本地查看此文档,请克隆此仓库,然后从此文件夹运行 cargo doc --open
。
变更日志
3.2.0
- 在
GenesisConfig
中引入block_prod_time
持续时间(以纳秒为单位),以定义产生块之间的持续时间。 - 从
RuntimeStandalone
中公开cur_block
和genesis_config
。这允许操作块时间。 - 使用
RuntimeConfig::from_protocol_version
来修复存储成本问题。 - 将根账户余额设置为十亿个代币。
入门指南
本节将指导您通过我们建议的方法将模拟测试添加到您的项目中。想要一个例子?请查看可交易代币示例。
依赖项版本
目前,这个包依赖于nearcore的GitHub仓库,因此这个包也必须是一个git依赖。此外,这个包的依赖项与构建Wasm智能合约冲突,因此您必须将其添加到以下位置
[dev-dependencies]
near-sdk-sim = "4.0.0-pre.9"
并且还要更新near-sdk
[dependencies]
near-sdk = "4.0.0-pre.9"
请注意,您需要添加版本的tag
(或commit
)。
工作空间设置
如果您想检查一个Rust合约的gas和存储使用情况,可以将上述依赖项添加到您的项目根目录下的Cargo.toml
中。如果您想测试跨合约调用,我们建议设置一个cargo 工作空间。以下是操作方法
假设您有一个名为contract
的文件夹中的现有合约项目。
在它里面创建一个名为contract
的子文件夹,并将contract
的原始内容移动到这个子文件夹中。现在您将拥有contract/contract
。如果您想,可以将根文件夹重命名为类似contracts
或contract-wrap
的名称。以下是一些bash命令来完成这项工作
mkdir contract-wrap
mv contract contract-wrap
现在在项目的根目录中(例如contract-wrap
),创建一个新的Cargo.toml
。您需要添加正常的[package]
部分,但与大多数项目不同,您没有dependencies
,只有dev-dependencies
和一个workspace
[dev-dependencies]
near-sdk = "4.0.0-pre.9"
near-sdk-sim = "4.0.0-pre.9"
contract = { path = "./contract" }
[workspace]
members = [
"contract"
]
现在,当您想要创建测试合约时,您可以在项目中添加一个新的子文件夹,并在根目录下的此Cargo.toml
中为其添加一行。
其他清理工作
- 您可以将嵌套项目的
[profile.release]
设置从嵌套项目移动到工作空间的根目录,因为工作空间成员会从工作空间根继承这些设置。 - 您还可以删除嵌套项目的
target
,因为所有工作空间成员都将构建到根项目的target
目录中 - 如果您使用的是
cargo build
进行构建,现在您可以一次性使用cargo build --all
构建所有工作空间成员
测试文件
在您的项目根目录中(例如上面示例中的contract-wrap
),创建一个包含Rust文件的tests
目录。这里的内容将被cargo test
自动运行。
在此文件夹内,为自己创建一个新的测试仓库存档,通过创建一个 tests/sim
目录和一个 tests/sim/main.rs
文件来实现。此文件将粘合(即组合)此文件夹内其他文件(即模块)。我们很快会向其中添加内容。目前您可以将其留空。
现在创建一个 tests/sim/utils.rs
文件。此文件将导出适用于所有测试的常用函数。在文件中,您需要包含想要测试的合约的字节。
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
// update `contract.wasm` for your contract's name
CONTRACT_WASM_BYTES => "target/wasm32-unknown-unknown/release/contract.wasm",
// if you run `cargo build` without `--release` flag:
CONTRACT_WASM_BYTES => "target/wasm32-unknown-unknown/debug/contract.wasm",
}
请注意,这意味着您必须先 build
,然后才能 test
!由于 cargo test
不会重新生成您的模拟测试所依赖的 wasm
文件,您需要在运行 cargo test
命令之前,使用 cargo build --all --target wasm32-unknown-unknown
。如果您对合约进行了修改,并且 您发誓它现在应该通过,请尝试重新构建!
现在您可以创建一个初始化模拟器的函数。
use near_sdk_sim::{init_simulator, to_yocto, STORAGE_AMOUNT};
const CONTRACT_ID: &str = "contract";
pub fn init() -> (UserAccount, UserAccount, UserAccount) {
// Use `None` for default genesis configuration; more info below
let root = init_simulator(None);
let contract = root.deploy(
&CONTRACT_WASM_BYTES,
CONTRACT_ID.to_string(),
STORAGE_AMOUNT // attached deposit
);
let alice = root.create_user(
"alice".parse().unwrap(),
to_yocto("100") // initial balance
);
(root, contract, alice)
}
现在您可以在 tests/sim/first_tests.rs
中添加一个使用此 init
函数的测试文件。对于您添加到此目录的每个文件,您都需要在 tests/sim/main.rs
中添加一行。让我们为到目前为止的两个文件都添加一行。
// in tests/sim/main.rs
mod utils;
mod first_tests;
现在向 first_tests.rs
中添加一些测试。
use near_sdk::serde_json::json;
use near_sdk_sim::DEFAULT_GAS;
use crate::utils::init;
#[test]
fn simulate_some_view_function() {
let (root, contract, _alice) = init();
let actual: String = root.view(
contract.account_id(),
"view_something",
&json!({
"some_param": "some_value".to_string(),
}).to_string().into_bytes(),
).unwrap_json();
assert_eq!("expected".to_string(), actual);
}
#[test]
fn simulate_some_change_method() {
let (root, contract, _alice) = init();
let result = root.call(
contract.account_id(),
"change_something",
json!({
"some_param": "some_value".to_string(),
}).to_string().into_bytes(),
DEFAULT_GAS,
1, // deposit
);
assert!(result.is_ok());
}
可选宏
上述方法是一个良好的起点,即使您的 Wasm 文件是从除 Rust 之外的语言编译的,它也能正常工作。
但是,如果您的原始文件是 Rust,并且您希望在测试时获得更好的用户体验,near-sdk-sim
提供了一个很好的额外功能。
near-sdk-sim
修改了 near_bindgen
宏,该宏来自 near-sdk
,以创建从您的合约生成的额外结构体+实现,并在名称末尾添加 Contract
,例如 xxxxxContract
。所以如果您有一个名称设置为 token
的合约,并在其 src/lib.rs
中设置了此名称
#[near_bindgen]
struct Token {
...
}
#[near_bindgen]
impl Token {
...
}
那么在您的模拟测试中,您可以导入 TokenContract
use token::TokenContract;
// or rename it maybe
use token::TokenContract as OtherNamedContract;
现在您可以简化上一节中的 init
及测试代码
// in utils.rs
use near_sdk_sim::{deploy, init_simulator, to_yocto, STORAGE_AMOUNT};
use token::TokenContract;
const CONTRACT_ID: &str = "contract";
pub fn init() -> (UserAccount, ContractAccount<TokenContract>, UserAccount) {
let root = init_simulator(None);
let contract = deploy!(
contract: TokenContract,
contract_id: CONTRACT_ID,
bytes: &CONTRACT_WASM_BYTES,
signer_account: root
);
let alice = root.create_user(
"alice".parse().unwrap(),
to_yocto("100") // initial balance
);
(root, contract, alice)
}
// in first_tests.rs
use near_sdk_sim::{call, view};
use crate::utils::init;
#[test]
fn simulate_some_view_function() {
let (root, contract, _alice) = init();
let actual: String = view!(
contract.view_something("some_value".to_string()),
).unwrap_json();
assert_eq!("expected", actual);
}
#[test]
fn simulate_some_change_method() {
let (root, contract, _alice) = init();
// uses default gas amount
let result = call!(
root,
contract.change_something("some_value".to_string()),
deposit = 1,
);
assert!(result.is_ok());
}
常见模式
分析燃气成本
对于由 call
或 call!
触发的交易链,您可以检查 gas_burnt
和 tokens_burnt
,其中 tokens_burnt
将等于 gas_burnt
乘以在创世配置中设置的 gas_price
。您还可以打印出 profile_data
以查看深入的燃气使用细分。
let outcome = some_account.call(
"some_contract",
"method",
&json({
"some_param": "some_value",
}).to_string().into_bytes(),
DEFAULT_GAS,
0,
);
println!(
"profile_data: {:#?} \n\ntokens_burnt: {}Ⓝ",
outcome.profile_data(),
(outcome.tokens_burnt()) as f64 / 1e24
);
let expected_gas_ceiling = 5 * u64::pow(10, 12); // 5 TeraGas
assert!(outcome.gas_burnt() < expected_gas_ceiling);
TeraGas 单位在这里 解释。
请记住,使用 --nocapture
运行测试以查看 println!
的输出。
cargo test -- --nocapture
此 println!
的输出可能如下所示
profile_data: ------------------------------
Total gas: 1891395594588
Host gas: 1595600369775 [84% total]
Action gas: 0 [0% total]
Wasm execution: 295795224813 [15% total]
------ Host functions --------
base -> 7678275219 [0% total, 0% host]
contract_compile_base -> 35445963 [0% total, 0% host]
contract_compile_bytes -> 48341969250 [2% total, 3% host]
read_memory_base -> 28708495200 [1% total, 1% host]
read_memory_byte -> 634822611 [0% total, 0% host]
write_memory_base -> 25234153749 [1% total, 1% host]
write_memory_byte -> 539306856 [0% total, 0% host]
read_register_base -> 20137321488 [1% total, 1% host]
read_register_byte -> 17938284 [0% total, 0% host]
write_register_base -> 25789702374 [1% total, 1% host]
write_register_byte -> 821137824 [0% total, 0% host]
utf8_decoding_base -> 3111779061 [0% total, 0% host]
utf8_decoding_byte -> 15162184908 [0% total, 0% host]
log_base -> 3543313050 [0% total, 0% host]
log_byte -> 686337132 [0% total, 0% host]
storage_write_base -> 192590208000 [10% total, 12% host]
storage_write_key_byte -> 1621105941 [0% total, 0% host]
storage_write_value_byte -> 2047223574 [0% total, 0% host]
storage_write_evicted_byte -> 2119742262 [0% total, 0% host]
storage_read_base -> 169070537250 [8% total, 10% host]
storage_read_key_byte -> 711908259 [0% total, 0% host]
storage_read_value_byte -> 370326330 [0% total, 0% host]
touching_trie_node -> 1046627135190 [55% total, 65% host]
------ Actions --------
------------------------------
tokens_burnt: 0.00043195379520539996Ⓝ
分析 存储 成本
使用 deploy!
创建的 ContractAccount
或使用 root.create_user
创建的 UserAccount
,您可以调用 account()
来获取存储在模拟区块链中的 Account 信息。
let account = root.account().unwrap();
let balance = account.amount;
let locked_in_stake = account.locked;
let storage_usage = account.storage_usage;
您可以使用这些信息来详细分析合约调用如何改变账户的存储使用情况。
检查复杂交易链中所有调用的中间状态
假设您有一个 call
或 call!
let outcome = some_account.call(
"some_contract",
"method",
&json({
"some_param": "some_value",
}).to_string().into_bytes(),
DEFAULT_GAS,
0,
);
如果这里的 some_contract.method
执行跨合约调用,near-sdk-sim
将允许所有这些调用完成。然后,您可以通过 outcome
结构体来检查整个调用链。一些有用的方法
您可以使用这些与 println!
和 美化打印插值 一起使用
println!("{:#?}", outcome.promise_results);
请记住,使用 --nocapture
运行您的测试以查看 println!
输出
cargo test -- --nocapture
您可能会看到如下内容
[
Some(
ExecutionResult {
outcome: ExecutionOutcome {
logs: [],
receipt_ids: [
`2bCDBfWgRkzGggXLuiXqhnVGbxwRz7RP3qa8WS5nNw8t`,
],
burnt_gas: 2428220615156,
tokens_burnt: 0,
status: SuccessReceiptId(2bCDBfWgRkzGggXLuiXqhnVGbxwRz7RP3qa8WS5nNw8t),
},
},
),
Some(
ExecutionResult {
outcome: ExecutionOutcome {
logs: [],
receipt_ids: [],
burnt_gas: 18841799405111,
tokens_burnt: 0,
status: Failure(Action #0: Smart contract panicked: panicked at 'Not an integer: ParseIntError { kind: InvalidDigit }', test-contract/src/lib.rs:85:56),
},
},
)
]
由于 ExecutionResult 还未包含合约或方法的名称,因此很难判断哪个调用是哪个。为了帮助调试,您可以在合约方法中使用 log!
。所有 log!
输出都将显示在上面的 ExecutionOutcomes 中的 logs
数组中。
检查预期的交易失败
如果您想检查上述调用链中某个交易的 logs
或 status
中的某些内容,您可以使用字符串匹配。要检查上述失败是否符合您的预期,您可以
use near_sdk_sim::transaction::ExecutionStatus;
#[test]
fn simulate_some_failure() {
let outcome = some_account.call(...);
assert_eq!(res.promise_errors().len(), 1);
if let ExecutionStatus::Failure(execution_error) =
&outcome.promise_errors().remove(0).unwrap().outcome().status
{
assert!(execution_error.to_string().contains("ParseIntError"));
} else {
unreachable!();
}
}
此 promise_errors
返回上述提到的 promise_results
方法的过滤版本。
解析 logs
非常简单,无论是从 get_receipt_results
还是从 logs
直接。
调整创世配置
对于许多模拟测试,使用 init_simulator(None)
就足够了。这使用的是 默认的创世配置设置
GenesisConfig {
genesis_time: 0,
gas_price: 100_000_000,
gas_limit: std::u64::MAX,
genesis_height: 0,
epoch_length: 3,
runtime_config: RuntimeConfig::default(),
state_records: vec![],
validators: vec![],
}
如果您想覆盖其中的一些值,例如模拟您的合约在 gas 价格 提高到 10 倍时的行为
use near_sdk_sim::runtime::GenesisConfig;
pub fn init () {
let mut genesis = GenesisConfig::default();
genesis.gas_price = genesis.gas_price * 10;
let root = init_simulator(Some(genesis));
}
依赖项
~65MB
~1M SLoC