要在合约项目目录中使用 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
文件并检查在 dev-dependencies
下使用的 fuels
的版本。如果尚未安装,请将版本更改为 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,您将获得大部分功能都包含在预导中。
use fuels::{prelude::*, types::{Identity, SizedAsciiString}};
在进行任何更改后始终编译您的合约。这样可以确保您使用的是最新生成的 contract-abi
。
在 abigen
宏中更新您的合约名称和 ABI 路径,以匹配您合约的名称:
// 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
既然我们对智能合约的功能感到自信,现在是时候构建前端了。这将允许用户与我们的新市场无缝交互!