Unit Tests
Unit testing is crucial for ensuring that your dApp behaves as expected before deploying it. In CrabRolls, you can create comprehensive unit tests to validate your dApp’s functionality. This guide will walk you through the process of setting up and writing unit tests for your dApp.
Writing Your First Unit Test
-
Create a New Test Module: Inside your application code, typically at the bottom of your
source code
or in a separatetests
module, you can define your tests.#[cfg(test)]mod tests {use super::*; // Import the main application codeuse crabrolls::prelude::*; // Import all utilities from CrabRolls} -
Define a Test Function: Tests in CrabRolls are asynchronous. Use the
async_std::test
attribute to define an async test function.#[async_std::test]async fn test_my_app() {// Test logic goes here} -
Create an Application Instance: Instantiate your application within the test function.
let app = MyApp::new(); -
Create a Tester Instance: Use the
Tester
utility to create a tester from your application instance. The tester helps you simulate and validate the application’s behavior.let tester = Tester::new(app, MockupOptions::default());By default the
MockupOptions
struct is used to configure the tester. You can customize the tester behavior by changing the options in theMockupOptions
struct. The struct has the following fields:- portal_config: The configuration of the portal used by the tester, a enum of PortalHandlerConfig with the options:
pub enum PortalHandlerConfig {Handle { advance: bool }, // Handle the portals and pass the payload to the app if advance is trueIgnore, // Ignore the deposit handle and pass the payload to the appDispense, // Dispense the deposit and discard the advance input}This will be used to configure the behavior of the tester when handling deposits, more about this in the Wallet abstraction testing section.
-
Prepare Input Payload: Next, prepare the input payload for the application based on the test scenario.
let payload = b"Hi CrabRolls!"; -
Simulate Application Advance: Use the tester to advance the state of your application with the prepared payload.
let result: AdvanceResult = tester.advance(Address::default(), payload).await;You can also inspect the application state:
let result: InspectResult = tester.inspect(payload).await; -
Validate Test Results: Check the results of the advance operation. Validate the status, errors, and outputs to ensure the application behaves as expected.
The output of the
advance
andinspect
operations is an object with the main fields to verify the application status, errors, and outputs. TheResult
object currently has the following fields:status
: The status of the application after the operation. You can get the status using theis_accepted
,is_rejected
to check if the application was accepted or rejected.error
: An optional error message if the application failed. You can check if the application failed using theis_errored
method and get the error message using theget_error
method.outputs
: A vector of outputs generated by the application. You can check the outputs vector using theget_outputs
method and validate the outputs content.metadata
: A metadata object with additional information about the application advance (only available in theadvance
operation). To get the metadata object, use theget_metadata
method. The metadata object has the following fields:input_index
: The index of the input that was processed.sender
: The address of the sender of the input.block_number
: The block number of the application.timestamp
: The timestamp of the input processing.
// Check the status of the resultassert!(result.is_accepted(), "Expected Accept status");// Check if there are any errors/// in this case, we expect no errors because the application should be acceptedassert!(!result.is_errored(), "Expected no error");// Validate the outputs lengthassert_eq!(result.get_outputs().len(),3,"Expected 3 outputs, got {}",result.get_outputs().len());// Validate the outputs contentassert_eq!(result.get_outputs(),vec![Output::Notice {payload: payload.to_vec()},Output::Report {payload: payload.to_vec()},Output::Voucher {destination: Address::default(),payload: payload.to_vec()}],"Expected outputs to match");// Validate the sender addressassert_eq!(result.get_metadata().sender,Address::default(),"Unexpected sender address");
Complete Test Case Example
Here’s an example of a complete test case following the steps above for an echo application like the Echo example:
#[cfg(test)]mod tests { use super::EchoApp; use crabrolls::prelude::*; // Import all utilities from CrabRolls use ethabi::Address;
#[async_std::test] async fn test_echo() { // Create an instance of your application let app = EchoApp::new();
// Create a tester instance let tester = Tester::new(app, MockupOptions::default());
// Prepare input payload let payload = b"Hi CrabRolls!";
// Simulate application advance let result = tester.advance(Address::default(), payload).await;
// Validate test results assert!(result.is_accepted(), "Expected Accept status"); assert!(!result.is_errored(), "Expected no error"); assert_eq!( result.get_outputs().len(), 3, "Expected 3 outputs, got {}", result.get_outputs().len() ); assert_eq!( result.get_outputs(), vec![ Output::Notice { payload: payload.to_vec() }, Output::Report { payload: payload.to_vec() }, Output::Voucher { destination: Address::default(), payload: payload.to_vec() } ], "Expected outputs to match" ); assert_eq!( result.get_metadata().sender, Address::default(), "Unexpected sender address" ); }}
Running Tests
To run your tests, you can use the cargo test
command. This command will compile your application and run all the tests defined in your application.
cargo test
The output will show the results of each test, including the status and any errors encountered during the test execution.
Compiling dapp v0.1.0 (/path/to/dapp) Finished `test` profile [unoptimized + debuginfo] target(s) in 0.92s Running unittests src/main.rs (target/debug/deps/dapp-xxx)
running 1 testtest tests::test_echo ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Wallet abstraction testing
When working with deposits in CrabRolls, you can test various scenarios involving Ether, ERC20, ERC721, and ERC1155 tokens. Below is a guide on setting up and writing unit tests for deposit and withdrawal operations within your dApp, specifically focusing on these types of assets.
The following steps illustrate how to write and execute tests for handling deposits (Ether, ERC20, ERC721, and ERC1155) and their corresponding functions, is be used the WalletAbstractionApp as base application.
Make deposit
The Tester
utility has a deposit
method that allows you to simulate deposits of Ether, ERC20, ERC721, and ERC1155 tokens. The deposit
method has the following signature:
deposit(deposit: Deposit) -> AdvanceResult
The Deposit
enum has the following variants:
pub enum Deposit { Ether { sender: Address, amount: Uint, }, ERC20 { sender: Address, token: Address, amount: Uint, }, ERC721 { sender: Address, token: Address, id: Uint, }, ERC1155 { sender: Address, token: Address, ids_amounts: Vec<(Uint, Uint)>, },}
Example usage on a test case:
#[async_std::test]async fn test_ether_deposit() { let app = WalletAbstractionApp::new(); let tester = Tester::new(app, MockupOptions::default());
let address = Address::default(); let amount = units::wei::from_ether(1.0);
// Call a deposit with Ether like a Portal input let deposit_result = tester .deposit(Deposit::Ether { sender: address, amount, }) .await;
assert!(deposit_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.ether_balance(address).await, amount, "Expected balance to match");}
Ether
Ether wallet abstraction has the following methods from the Tester
utility:
// Get all existing wallet addresses on the application.ether_addresses() -> Vec<Address>
// Get the Ether balance of an address on the wallet application.ether_balance(wallet: Address) -> Uint
// Transfer Ether from one wallet to another wallet on the application wallet.ether_transfer(source: Address, destination: Address, amount: Uint) -> Result<(), Box<dyn Error>>
Example usage on a test case:
#[async_std::test]async fn test_ether() { let app = WalletAbstractionApp::new(); let tester = Tester::new(app, MockupOptions::default());
let address = Address::default(); let amount = units::wei::from_ether(6.0);
// Deposit Ether let deposit_result = tester .deposit(Deposit::Ether { sender: address, amount, }) .await;
assert!(deposit_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.ether_balance(address).await, amount, "Expected balance to match");
// Transfer Ether let recipient = Address::from_low_u64_be(1); let transfer_result = tester.ether_transfer(address, recipient, amount / 2).await;
assert!(transfer_result.is_ok(), "Expected Ok status"); assert_eq!(tester.ether_balance(address).await, amount / 2, "Expected balance to match"); assert_eq!(tester.ether_balance(recipient).await, amount / 2, "Expected balance to match"); assert_eq!(tester.ether_addresses().await, vec![address, recipient], "Expected addresses to match");
// Withdraw Ether let withdraw_payload = json!({ "kind": "ether", "metadata": {} }) .to_string();
let advance_result = tester.advance(address, withdraw_payload).await;
assert!(advance_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.ether_balance(address).await, Uint::zero(), "Expected balance to be zero"); assert_eq!(advance_result.get_outputs().len(), 1, "Expected one output of voucher withdrawal"); assert_eq!(tester.ether_addresses().await, vec![recipient], "Expected addresses to match");}
ERC20
ERC20 wallet abstraction has the following methods from the Tester
utility:
// Get all existing wallet addresses on the application.erc20_addresses() -> Vec<Address>
// Get the ERC20 balance of an address on the wallet application.erc20_balance(wallet: Address, token: Address) -> Uint
// Transfer ERC20 tokens from one wallet to another wallet on the application wallet.erc20_transfer(source: Address, destination: Address, token: Address, amount: Uint) -> Result<(), Box<dyn Error>>
Example usage on a test case:
#[async_std::test]async fn test_erc20() { let app = WalletAbstractionApp::new(); let tester = Tester::new(app, MockupOptions::default());
let address = Address::default(); let token_address = Address::from_low_u64_be(1); let amount = uint!(1000u64);
// Deposit ERC20 tokens let deposit_result = tester .deposit(Deposit::ERC20 { sender: address, token: token_address, amount, }) .await;
assert!(deposit_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.erc20_balance(address, token_address).await, amount);
// Transfer ERC20 tokens let recipient = Address::from_low_u64_be(1); let transfer_result = tester.erc20_transfer(address, recipient, token_address, amount / 2).await;
assert!(transfer_result.is_ok(), "Expected Ok status"); assert_eq!(tester.erc20_balance(address, token_address).await, amount / 2, "Expected balance to match"); assert_eq!(tester.erc20_balance(recipient, token_address).await, amount / 2, "Expected balance to match"); assert_eq!(tester.erc20_addresses().await, vec![address, recipient], "Expected addresses to match");
// Withdraw ERC20 tokens let withdraw_payload = json!({ "kind": "erc20", "metadata": { "token": token_address } }) .to_string();
let advance_result = tester.advance(address, withdraw_payload).await;
assert!(advance_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.erc20_balance(address, token_address).await, Uint::zero(), "Expected balance to be zero"); assert_eq!(advance_result.get_outputs().len(), 1, "Expected one output of voucher withdrawal"); assert_eq!(tester.erc20_addresses().await, vec![recipient], "Expected addresses to match");}
ERC721
ERC721 wallet abstraction has the following methods from the Tester
utility:
// Get all existing wallet addresses on the application.erc721_addresses() -> Vec<Address>
// Transfer ERC721 tokens from one wallet to another wallet on the application wallet.erc721_transfer(source: Address, destination: Address, token: Address, id: Uint) -> Result<(), Box<dyn Error>>
// Get the owner of an ERC721 token on the application.erc721_owner_of(token: Address, id: Uint) -> Option<Address>
Example usage on a test case:
#[async_std::test]async fn test_erc721() { let app = WalletAbstractionApp::new(); let tester = Tester::new(app, MockupOptions::default());
let address = Address::default(); let token_address = Address::from_low_u64_be(1); let token_id = uint!(1u64);
// Deposit ERC721 token let deposit_result = tester .deposit(Deposit::ERC721 { sender: address, token: token_address, id: token_id, }) .await;
assert!(deposit_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.erc721_owner_of(token_address, token_id).await, Some(address), "Expected owner to match"); assert_eq!(tester.erc721_addresses().await, vec![address], "Expected addresses to match");
// Transfer ERC721 token let recipient = Address::from_low_u64_be(1); let transfer_result = tester.erc721_transfer(address, recipient, token_address, token_id).await;
assert!(transfer_result.is_ok(), "Expected Ok status"); assert_eq!(tester.erc721_owner_of(token_address, token_id).await, Some(recipient), "Expected owner to match"); assert_eq!(tester.erc721_addresses().await, vec![recipient], "Expected addresses to match");
// Withdraw ERC721 token let withdraw_payload = json!({ "kind": "erc721", "metadata": { "token": token_address, "id": token_id } }) .to_string();
let advance_result = tester.advance(recipient, withdraw_payload).await;
assert!(advance_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.erc721_owner_of(token_address, token_id).await, None, "Expected owner to be none"); assert_eq!(advance_result.get_outputs().len(), 1, "Expected one output of voucher withdrawal"); assert_eq!(tester.erc721_addresses().await, vec![], "Expected addresses to match");}
ERC1155
ERC1155 wallet abstraction has the following methods from the Tester
utility:
// Get all existing wallet addresses on the application.erc1155_addresses() -> Vec<Address>
// Get the ERC1155 balance of an address on the wallet application.erc1155_balance(wallet: Address, token: Address, id: Uint) -> Uint
// Transfer ERC1155 tokens from one wallet to another wallet on the application wallet. transfers is a vector of (id, amount) tuples.erc1155_transfer(source: Address, destination: Address, token: Address, transfers: Vec<(Uint, Uint)>) -> Result<(), Box<dyn Error>>
Example usage on a test case:
#[async_std::test]async fn test_erc1155() { let app = WalletAbstractionApp::new(); let tester = Tester::new(app, MockupOptions::default());
let address = Address::default(); let token_address = Address::from_low_u64_be(1); let token_id = uint!(1u64); let amount = uint!(10u64);
// Deposit ERC1155 tokens let deposit_result = tester .deposit(Deposit::ERC1155 { sender: address, token: token_address, ids_amounts: vec![(token_id, amount)], }) .await;
assert!(deposit_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.erc1155_balance(address, token_address, token_id).await, amount); assert_eq!(tester.erc1155_addresses().await, vec![address], "Expected addresses to match");
// Transfer ERC1155 tokens let recipient = Address::from_low_u64_be(1); let transfer_result = tester.erc1155_transfer(address, recipient, token_address, vec![(token_id, amount / 2)]).await;
assert!(transfer_result.is_ok(), "Expected Ok status"); assert_eq!(tester.erc1155_balance(address, token_address, token_id).await, amount / 2, "Expected balance to match"); assert_eq!(tester.erc1155_balance(recipient, token_address, token_id).await, amount / 2, "Expected balance to match"); assert_eq!(tester.erc1155_addresses().await, vec![address, recipient], "Expected addresses to match");
// Withdraw ERC1155 tokens let withdraw_payload = json!({ "kind": "erc1155", "metadata": { "token": token_address, "ids": [token_id], "data": null } }) .to_string();
let advance_result = tester.advance(address, withdraw_payload).await;
assert!(advance_result.is_accepted(), "Expected Accept status"); assert_eq!(tester.erc1155_balance(address, token_address, token_id).await, Uint::zero(), "Expected balance to be zero"); assert_eq!(advance_result.get_outputs().len(), 1, "Expected one output of voucher withdrawal");}