测试合约
在 Rust 中生成测试模板
要使用 Rust 创建您自己的测试模板,请按照以下步骤操作 cargo-generate 在合同项目目录中:
- 安装 cargo-generate:
cargo install cargo-generate --locked
- 生成模板:
cargo generate --init fuellabs/sway templates/sway-test-rs --name sway-store
我们有两个新文件!
这 Cargo.toml 是我们新测试工具的清单,并指定所需的依赖项,包括 fuels (Fuel Rust SDK)。 这 tests/harness.rs 包含一些让我们开始的样板测试代码,但尚未调用任何合约方法。 打开你的 Cargo.toml 文件并检查版本 fuels 在下使用 dev-dependencies。将版本更改为 0.62.0 如果还没有:
[dev-dependencies]
fuels = { version = "0.62.0", features = ["fuel-core-lib"] }
tokio = { version = "1.12", features = ["rt", "macros"] }
导入
我们将改变现有的 harness.rs 已经生成测试文件。首先我们需要改变进口。通过导入 Fuel Rust SDK,您将获得 Prelude 中包含的大部分功能。
use fuels::{prelude::*, types::{Identity, SizedAsciiString}};
在进行任何更改后,请务必编译您的合同。这可确保您使用的是最新的contract-abi生成。
在abigen宏以匹配您的合约名称:
// Load abi from json
abigen!(Contract(name="SwayStore", abi="out/debug/contract-abi.json"));
初始化函数
在为 Sway 编写测试时,需要两个关键对象:合约实例和与之交互的钱包。此帮助程序功能可确保每个新测试用例都重新开始,因此请将其复制到测试文件中。为此,它将导出已部署的合约、合约 ID 和所有生成的钱包。
将get_contract_instance在测试工具中具有以下功能的功能:
async fn get_contract_instance() -> (SwayStore<WalletUnlocked>, ContractId, Vec<WalletUnlocked>) {
// Launch a local network and deploy the contract
let wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(
Some(3), /* Three wallets */
Some(1), /* Single coin (UTXO) */
Some(1_000_000_000), /* Amount per coin */
),
None,
None,
)
.await
.unwrap();
let wallet = wallets.get(0).unwrap().clone();
let id = Contract::load_from(
"./out/debug/contract.bin",
LoadConfiguration::default(),
)
.unwrap()
.deploy(&wallet, TxPolicies::default())
.await
.unwrap();
let instance = SwayStore::new(id.clone(), wallet);
(instance, id.into(), wallets)
}
测试用例
鉴于智能合约的不可变性,在测试中涵盖所有潜在的边缘情况非常重要。 让我们删除示例can_get_contract_id测试用例,并开始在我们的底部编写一些测试用例harness.rs文件。
设置所有者
对于此测试用例,我们使用合约实例并使用 SDK 的.with_account()方法。这让我们可以模拟第一个钱包。为了检查所有者是否设置正确,我们可以查看合约给出的地址是否与钱包 1 的地址匹配。如果您想更深入地挖掘,查看合约存储将显示钱包 1 的地址是否正确存储。
#[tokio::test]
async fn can_set_owner() {
let (instance, _id, wallets) = get_contract_instance().await;
// get access to a test wallet
let wallet_1 = wallets.get(0).unwrap();
// initialize wallet_1 as the owner
let owner_result = instance
.with_account(wallet_1.clone())
.methods()
.initialize_owner()
.call()
.await
.unwrap();
// make sure the returned identity matches wallet_1
assert!(Identity::Address(wallet_1.address().into()) == owner_result.value);
}
设置所有者一次
我们需要警惕的一个边缘情况是尝试两次设置所有者。我们当然不希望未经授权转让我们的合同所有权!为了解决这个问题,我们在 Sway 合约中包含了以下行:require(owner.is_none(), "owner already initialized"); 这可确保只有在之前未建立所有者时才能设置所有者。为了测试这一点,我们创建了一个新的合约实例:最初,我们使用钱包 1 设置所有者。任何后续尝试使用钱包 2 设置所有者都应该不成功。
#[tokio::test]
#[should_panic]
async fn can_set_owner_only_once() {
let (instance, _id, wallets) = get_contract_instance().await;
// get access to some test wallets
let wallet_1 = wallets.get(0).unwrap();
let wallet_2 = wallets.get(1).unwrap();
// initialize wallet_1 as the owner
let _owner_result = instance.clone()
.with_account(wallet_1.clone())
.methods()
.initialize_owner()
.call()
.await
.unwrap();
// this should fail
// try to set the owner from wallet_2
let _fail_owner_result = instance.clone()
.with_account(wallet_2.clone())
.methods()
.initialize_owner()
.call()
.await
.unwrap();
}
在市场上买卖
测试智能合约的基本功能以确保其正常运行至关重要。 对于此测试,我们设置了两个钱包:
第一个钱包发起交易以列出待售物品。这是通过调用.list_item()方法,指定他们出售的商品的价格和详细信息。 第二个钱包继续使用.buy_item()方法,提供他们打算购买的物品的索引。 在这些交易之后,我们将评估两个钱包的余额,以确认交易的成功执行。
#[tokio::test]
async fn can_list_and_buy_item() {
let (instance, _id, wallets) = get_contract_instance().await;
// Now you have an instance of your contract you can use to test each function
// get access to some test wallets
let wallet_1 = wallets.get(0).unwrap();
let wallet_2 = wallets.get(1).unwrap();
// item 1 params
let item_1_metadata: SizedAsciiString<20> = "metadata__url__here_"
.try_into()
.expect("Should have succeeded");
let item_1_price: u64 = 15;
// list item 1 from wallet_1
let _item_1_result = instance.clone()
.with_account(wallet_1.clone())
.methods()
.list_item(item_1_price, item_1_metadata)
.call()
.await
.unwrap();
// call params to send the project price in the buy_item fn
let call_params = CallParameters::default().with_amount(item_1_price);
// buy item 1 from wallet_2
let _item_1_purchase = instance.clone()
.with_account(wallet_2.clone())
.methods()
.buy_item(1)
.append_variable_outputs(1)
.call_params(call_params)
.unwrap()
.call()
.await
.unwrap();
// check the balances of wallet_1 and wallet_2
let balance_1: u64 = wallet_1.get_asset_balance(&AssetId::zeroed()).await.unwrap();
let balance_2: u64 = wallet_2.get_asset_balance(&AssetId::zeroed()).await.unwrap();
// make sure the price was transferred from wallet_2 to wallet_1
assert!(balance_1 == 1000000015);
assert!(balance_2 == 999999985);
let item_1 = instance.methods().get_item(1).call().await.unwrap();
assert!(item_1.value.price == item_1_price);
assert!(item_1.value.id == 1);
assert!(item_1.value.total_bought == 1);
}
提取业主费用
最重要的是,作为市场的创造者,您需要确保自己得到补偿。与前面的测试类似,我们将调用相关函数进行交换。这一次,我们将验证您是否可以提取资金差额。
#[tokio::test]
async fn can_withdraw_funds() {
let (instance, _id, wallets) = get_contract_instance().await;
// Now you have an instance of your contract you can use to test each function
// get access to some test wallets
let wallet_1 = wallets.get(0).unwrap();
let wallet_2 = wallets.get(1).unwrap();
let wallet_3 = wallets.get(2).unwrap();
// initialize wallet_1 as the owner
let owner_result = instance.clone()
.with_account(wallet_1.clone())
.methods()
.initialize_owner()
.call()
.await
.unwrap();
// make sure the returned identity matches wallet_1
assert!(Identity::Address(wallet_1.address().into()) == owner_result.value);
// item 1 params
let item_1_metadata: SizedAsciiString<20> = "metadata__url__here_"
.try_into()
.expect("Should have succeeded");
let item_1_price: u64 = 150_000_000;
// list item 1 from wallet_2
let item_1_result = instance.clone()
.with_account(wallet_2.clone())
.methods()
.list_item(item_1_price, item_1_metadata)
.call()
.await;
assert!(item_1_result.is_ok());
// make sure the item count increased
let count = instance.clone()
.methods()
.get_count()
.simulate()
.await
.unwrap();
assert_eq!(count.value, 1);
// call params to send the project price in the buy_item fn
let call_params = CallParameters::default().with_amount(item_1_price);
// buy item 1 from wallet_3
let item_1_purchase = instance.clone()
.with_account(wallet_3.clone())
.methods()
.buy_item(1)
.append_variable_outputs(1)
.call_params(call_params)
.unwrap()
.call()
.await;
assert!(item_1_purchase.is_ok());
// make sure the item's total_bought count increased
let listed_item = instance
.methods()
.get_item(1)
.simulate()
.await
.unwrap();
assert_eq!(listed_item.value.total_bought, 1);
// withdraw the balance from the owner's wallet
let withdraw = instance
.with_account(wallet_1.clone())
.methods()
.withdraw_funds()
.append_variable_outputs(1)
.call()
.await;
assert!(withdraw.is_ok());
// check the balances of wallet_1 and wallet_2
let balance_1: u64 = wallet_1.get_asset_balance(&AssetId::zeroed()).await.unwrap();
let balance_2: u64 = wallet_2.get_asset_balance(&AssetId::zeroed()).await.unwrap();
let balance_3: u64 = wallet_3.get_asset_balance(&AssetId::zeroed()).await.unwrap();
assert!(balance_1 == 1007500000);
assert!(balance_2 == 1142500000);
assert!(balance_3 == 850000000);
}
检查
如果您已正确执行上述步骤,则您的harness.rs测试文件应如下所示:
use fuels::{prelude::*, types::{Identity, SizedAsciiString}};
// Load abi from json
abigen!(Contract(name="SwayStore", abi="out/debug/contract-abi.json"));
async fn get_contract_instance() -> (SwayStore<WalletUnlocked>, ContractId, Vec<WalletUnlocked>) {
// Launch a local network and deploy the contract
let wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(
Some(3), /* Three wallets */
Some(1), /* Single coin (UTXO) */
Some(1_000_000_000), /* Amount per coin */
),
None,
None,
)
.await
.unwrap();
let wallet = wallets.get(0).unwrap().clone();
let id = Contract::load_from(
"./out/debug/contract.bin",
LoadConfiguration::default(),
)
.unwrap()
.deploy(&wallet, TxPolicies::default())
.await
.unwrap();
let instance = SwayStore::new(id.clone(), wallet);
(instance, id.into(), wallets)
}
#[tokio::test]
async fn can_set_owner() {
let (instance, _id, wallets) = get_contract_instance().await;
// get access to a test wallet
let wallet_1 = wallets.get(0).unwrap();
// initialize wallet_1 as the owner
let owner_result = instance
.with_account(wallet_1.clone())
.methods()
.initialize_owner()
.call()
.await
.unwrap();
// make sure the returned identity matches wallet_1
assert!(Identity::Address(wallet_1.address().into()) == owner_result.value);
}
#[tokio::test]
#[should_panic]
async fn can_set_owner_only_once() {
let (instance, _id, wallets) = get_contract_instance().await;
// get access to some test wallets
let wallet_1 = wallets.get(0).unwrap();
let wallet_2 = wallets.get(1).unwrap();
// initialize wallet_1 as the owner
let _owner_result = instance.clone()
.with_account(wallet_1.clone())
.methods()
.initialize_owner()
.call()
.await
.unwrap();
// this should fail
// try to set the owner from wallet_2
let _fail_owner_result = instance.clone()
.with_account(wallet_2.clone())
.methods()
.initialize_owner()
.call()
.await
.unwrap();
}
#[tokio::test]
async fn can_list_and_buy_item() {
let (instance, _id, wallets) = get_contract_instance().await;
// Now you have an instance of your contract you can use to test each function
// get access to some test wallets
let wallet_1 = wallets.get(0).unwrap();
let wallet_2 = wallets.get(1).unwrap();
// item 1 params
let item_1_metadata: SizedAsciiString<20> = "metadata__url__here_"
.try_into()
.expect("Should have succeeded");
let item_1_price: u64 = 15;
// list item 1 from wallet_1
let _item_1_result = instance.clone()
.with_account(wallet_1.clone())
.methods()
.list_item(item_1_price, item_1_metadata)
.call()
.await
.unwrap();
// call params to send the project price in the buy_item fn
let call_params = CallParameters::default().with_amount(item_1_price);
// buy item 1 from wallet_2
let _item_1_purchase = instance.clone()
.with_account(wallet_2.clone())
.methods()
.buy_item(1)
.append_variable_outputs(1)
.call_params(call_params)
.unwrap()
.call()
.await
.unwrap();
// check the balances of wallet_1 and wallet_2
let balance_1: u64 = wallet_1.get_asset_balance(&AssetId::zeroed()).await.unwrap();
let balance_2: u64 = wallet_2.get_asset_balance(&AssetId::zeroed()).await.unwrap();
// make sure the price was transferred from wallet_2 to wallet_1
assert!(balance_1 == 1000000015);
assert!(balance_2 == 999999985);
let item_1 = instance.methods().get_item(1).call().await.unwrap();
assert!(item_1.value.price == item_1_price);
assert!(item_1.value.id == 1);
assert!(item_1.value.total_bought == 1);
}
#[tokio::test]
async fn can_withdraw_funds() {
let (instance, _id, wallets) = get_contract_instance().await;
// Now you have an instance of your contract you can use to test each function
// get access to some test wallets
let wallet_1 = wallets.get(0).unwrap();
let wallet_2 = wallets.get(1).unwrap();
let wallet_3 = wallets.get(2).unwrap();
// initialize wallet_1 as the owner
let owner_result = instance.clone()
.with_account(wallet_1.clone())
.methods()
.initialize_owner()
.call()
.await
.unwrap();
// make sure the returned identity matches wallet_1
assert!(Identity::Address(wallet_1.address().into()) == owner_result.value);
// item 1 params
let item_1_metadata: SizedAsciiString<20> = "metadata__url__here_"
.try_into()
.expect("Should have succeeded");
let item_1_price: u64 = 150_000_000;
// list item 1 from wallet_2
let item_1_result = instance.clone()
.with_account(wallet_2.clone())
.methods()
.list_item(item_1_price, item_1_metadata)
.call()
.await;
assert!(item_1_result.is_ok());
// make sure the item count increased
let count = instance.clone()
.methods()
.get_count()
.simulate()
.await
.unwrap();
assert_eq!(count.value, 1);
// call params to send the project price in the buy_item fn
let call_params = CallParameters::default().with_amount(item_1_price);
// buy item 1 from wallet_3
let item_1_purchase = instance.clone()
.with_account(wallet_3.clone())
.methods()
.buy_item(1)
.append_variable_outputs(1)
.call_params(call_params)
.unwrap()
.call()
.await;
assert!(item_1_purchase.is_ok());
// make sure the item's total_bought count increased
let listed_item = instance
.methods()
.get_item(1)
.simulate()
.await
.unwrap();
assert_eq!(listed_item.value.total_bought, 1);
// withdraw the balance from the owner's wallet
let withdraw = instance
.with_account(wallet_1.clone())
.methods()
.withdraw_funds()
.append_variable_outputs(1)
.call()
.await;
assert!(withdraw.is_ok());
// check the balances of wallet_1 and wallet_2
let balance_1: u64 = wallet_1.get_asset_balance(&AssetId::zeroed()).await.unwrap();
let balance_2: u64 = wallet_2.get_asset_balance(&AssetId::zeroed()).await.unwrap();
let balance_3: u64 = wallet_3.get_asset_balance(&AssetId::zeroed()).await.unwrap();
assert!(balance_1 == 1007500000);
assert!(balance_2 == 1142500000);
assert!(balance_3 == 850000000);
}
运行测试
运行位于tests/harness.rs,在您的contract文件夹:
cargo test
如果要在测试期间将输出打印到控制台,请使用 nocapture 标记:
cargo test -- --nocapture
现在我们对智能合约的功能充满信心,是时候构建一个前端了。这将使用户能够与我们的新市场无缝互动!