We will be changing the existing harness.rs test file that has been generated. Firstly we need to change the imports. By importing the Fuel Rust SDK you will get majority of the functionalities housed within the prelude.
use fuels::{prelude::*, types::{Identity, SizedAsciiString}};
Always compile your contracts after making any changes. This ensures you're working with the most recent contract-abi that gets generated.
Update your contract name and ABI path in the abigen macro to match the name of your contract:
// Load abi from jsonabigen!(Contract(name="SwayStore", abi="out/debug/contract-abi.json"));
When writing tests for Sway, two crucial objects are required: the contract instance and the wallets that interact with it. This helper function ensures a fresh start for every new test case so copy this into your test file. It will export the deployed contracts, the contract ID, and all the generated wallets for this purpose.
Replace the get_contract_instance function in your test harness with the function below:
asyncfnget_contract_instance() -> (SwayStore<WalletUnlocked>, ContractId, Vec<WalletUnlocked>) { // Launch a local network and deploy the contractlet 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)}
Given the immutable nature of smart contracts, it's important to cover all potential edge cases in your tests.
Let's write the test cases at the bottom of our harness.rs file.
For this test case, we use the contract instance and use the SDK's .with_account() method. This lets us impersonate the first wallet. To check if the owner has been set correctly, we can see if the address given by the contract matches wallet 1's address. If you want to dig deeper, looking into the contract storage will show if wallet 1's address is stored properly.
#[tokio::test]asyncfncan_set_owner() {let (instance, _id, wallets) =get_contract_instance().await; // get access to a test walletlet wallet_1 = wallets.get(0).unwrap(); // initialize wallet_1 as the ownerlet owner_result = instance.with_account(wallet_1.clone()).unwrap().methods().initialize_owner().call().await.unwrap(); // make sure the returned identity matches wallet_1assert!(Identity::Address(wallet_1.address().into()) == owner_result.value);}
An edge case we need to be vigilant about is an attempt to set the owner twice. We certainly don't want unauthorized ownership transfer of our contract! To address this, we've included the following line in our Sway contract: require(owner.is_none(), "owner already initialized");
This ensures the owner can only be set when it hasn't been previously established. To test this, we create a new contract instance: initially, we set the owner using wallet 1. Any subsequent attempt to set the owner with wallet 2 should be unsuccessful.
#[tokio::test]#[should_panic]asyncfncan_set_owner_only_once() {let (instance, _id, wallets) =get_contract_instance().await; // get access to some test walletslet wallet_1 = wallets.get(0).unwrap();let wallet_2 = wallets.get(1).unwrap(); // initialize wallet_1 as the ownerlet _owner_result = instance.with_account(wallet_1.clone()).unwrap().methods().initialize_owner().call().await.unwrap(); // this should fail // try to set the owner from wallet_2let _fail_owner_result = instance.with_account(wallet_2.clone()).unwrap().methods().initialize_owner().call().await.unwrap();}
It's essential to test the basic functionalities of a smart contract to ensure its proper operation.
For this test, we have two wallets set up:
The first wallet initiates a transaction to list an item for sale. This is done by calling the .list_item() method, specifying both the price and details of the item they're selling.
The second wallet proceeds to purchase the listed item using the .buy_item() method, providing the index of the item they intend to buy.
Following these transactions, we'll assess the balances of both wallets to confirm the successful execution of the transactions.
#[tokio::test]asyncfncan_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 walletslet wallet_1 = wallets.get(0).unwrap();let wallet_2 = wallets.get(1).unwrap(); // item 1 paramslet 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_1let _item_1_result = instance.with_account(wallet_1.clone()).unwrap().methods().list_item(item_1_price, item_1_metadata).call().await.unwrap(); // call params to send the project price in the buy_item fnlet call_params =CallParameters::default().with_amount(item_1_price); // buy item 1 from wallet_2let _item_1_purchase = instance.with_account(wallet_2.clone()).unwrap().methods().buy_item(1).append_variable_outputs(1).call_params(call_params).unwrap().call().await.unwrap(); // check the balances of wallet_1 and wallet_2let balance_1:u64= wallet_1.get_asset_balance(&BASE_ASSET_ID).await.unwrap();let balance_2:u64= wallet_2.get_asset_balance(&BASE_ASSET_ID).await.unwrap(); // make sure the price was transferred from wallet_2 to wallet_1assert!(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);}
Most importantly, as the creator of the marketplace, you need to ensure you're compensated. Similar to the previous tests, we'll invoke the relevant functions to make an exchange. This time, we'll verify if you can extract the difference in funds.
#[tokio::test]asyncfncan_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 walletslet 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 ownerlet owner_result = instance.with_account(wallet_1.clone()).unwrap().methods().initialize_owner().call().await.unwrap(); // make sure the returned identity matches wallet_1assert!(Identity::Address(wallet_1.address().into()) == owner_result.value); // item 1 paramslet 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_2let item_1_result = instance.with_account(wallet_2.clone()).unwrap().methods().list_item(item_1_price, item_1_metadata).call().await;assert!(item_1_result.is_ok()); // make sure the item count increasedlet count = instance.methods().get_count().simulate().await.unwrap();assert_eq!(count.value, 1); // call params to send the project price in the buy_item fnlet call_params =CallParameters::default().with_amount(item_1_price); // buy item 1 from wallet_3let item_1_purchase = instance.with_account(wallet_3.clone()).unwrap().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 increasedlet 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 walletlet withdraw = instance.with_account(wallet_1.clone()).unwrap().methods().withdraw_funds().append_variable_outputs(1).call().await;assert!(withdraw.is_ok()); // check the balances of wallet_1 and wallet_2let balance_1:u64= wallet_1.get_asset_balance(&BASE_ASSET_ID).await.unwrap();let balance_2:u64= wallet_2.get_asset_balance(&BASE_ASSET_ID).await.unwrap();let balance_3:u64= wallet_3.get_asset_balance(&BASE_ASSET_ID).await.unwrap();assert!(balance_1 ==1007500000);assert!(balance_2 ==1142500000);assert!(balance_3 ==850000000);}
If you have followed the previous steps correctly your harness.rs test file should look like this:
use fuels::{prelude::*, types::{Identity, SizedAsciiString}};// Load abi from jsonabigen!(Contract(name="SwayStore", abi="out/debug/contract-abi.json"));asyncfnget_contract_instance() -> (SwayStore<WalletUnlocked>, ContractId, Vec<WalletUnlocked>) { // Launch a local network and deploy the contractlet 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]asyncfncan_set_owner() {let (instance, _id, wallets) =get_contract_instance().await; // get access to a test walletlet wallet_1 = wallets.get(0).unwrap(); // initialize wallet_1 as the ownerlet owner_result = instance.with_account(wallet_1.clone()).unwrap().methods().initialize_owner().call().await.unwrap(); // make sure the returned identity matches wallet_1assert!(Identity::Address(wallet_1.address().into()) == owner_result.value);}#[tokio::test]#[should_panic]asyncfncan_set_owner_only_once() {let (instance, _id, wallets) =get_contract_instance().await; // get access to some test walletslet wallet_1 = wallets.get(0).unwrap();let wallet_2 = wallets.get(1).unwrap(); // initialize wallet_1 as the ownerlet _owner_result = instance.with_account(wallet_1.clone()).unwrap().methods().initialize_owner().call().await.unwrap(); // this should fail // try to set the owner from wallet_2let _fail_owner_result = instance.with_account(wallet_2.clone()).unwrap().methods().initialize_owner().call().await.unwrap();}#[tokio::test]asyncfncan_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 walletslet wallet_1 = wallets.get(0).unwrap();let wallet_2 = wallets.get(1).unwrap(); // item 1 paramslet 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_1let _item_1_result = instance.with_account(wallet_1.clone()).unwrap().methods().list_item(item_1_price, item_1_metadata).call().await.unwrap(); // call params to send the project price in the buy_item fnlet call_params =CallParameters::default().with_amount(item_1_price); // buy item 1 from wallet_2let _item_1_purchase = instance.with_account(wallet_2.clone()).unwrap().methods().buy_item(1).append_variable_outputs(1).call_params(call_params).unwrap().call().await.unwrap(); // check the balances of wallet_1 and wallet_2let balance_1:u64= wallet_1.get_asset_balance(&BASE_ASSET_ID).await.unwrap();let balance_2:u64= wallet_2.get_asset_balance(&BASE_ASSET_ID).await.unwrap(); // make sure the price was transferred from wallet_2 to wallet_1assert!(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]asyncfncan_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 walletslet 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 ownerlet owner_result = instance.with_account(wallet_1.clone()).unwrap().methods().initialize_owner().call().await.unwrap(); // make sure the returned identity matches wallet_1assert!(Identity::Address(wallet_1.address().into()) == owner_result.value); // item 1 paramslet 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_2let item_1_result = instance.with_account(wallet_2.clone()).unwrap().methods().list_item(item_1_price, item_1_metadata).call().await;assert!(item_1_result.is_ok()); // make sure the item count increasedlet count = instance.methods().get_count().simulate().await.unwrap();assert_eq!(count.value, 1); // call params to send the project price in the buy_item fnlet call_params =CallParameters::default().with_amount(item_1_price); // buy item 1 from wallet_3let item_1_purchase = instance.with_account(wallet_3.clone()).unwrap().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 increasedlet 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 walletlet withdraw = instance.with_account(wallet_1.clone()).unwrap().methods().withdraw_funds().append_variable_outputs(1).call().await;assert!(withdraw.is_ok()); // check the balances of wallet_1 and wallet_2let balance_1:u64= wallet_1.get_asset_balance(&BASE_ASSET_ID).await.unwrap();let balance_2:u64= wallet_2.get_asset_balance(&BASE_ASSET_ID).await.unwrap();let balance_3:u64= wallet_3.get_asset_balance(&BASE_ASSET_ID).await.unwrap();assert!(balance_1 ==1007500000);assert!(balance_2 ==1142500000);assert!(balance_3 ==850000000);}
If you want to print outputs to the console during tests, use the nocapture flag:
cargo test -- --nocapture
Now that we're confident in the functionality of our smart contract, it's time to build a frontend. This will allow users to seamlessly interact with our new marketplace!