Skip to content

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

  1. Create a New Test Module: Inside your application code, typically at the bottom of your source code or in a separate tests module, you can define your tests.

    #[cfg(test)]
    mod tests {
    use super::*; // Import the main application code
    use crabrolls::prelude::*; // Import all utilities from CrabRolls
    }
  2. 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
    }
  3. Create an Application Instance: Instantiate your application within the test function.

    let app = MyApp::new();
  4. 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 the MockupOptions 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 true
    Ignore, // Ignore the deposit handle and pass the payload to the app
    Dispense, // 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.

  5. Prepare Input Payload: Next, prepare the input payload for the application based on the test scenario.

    let payload = b"Hi CrabRolls!";
  6. 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;
  7. 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 and inspect operations is an object with the main fields to verify the application status, errors, and outputs. The Result object currently has the following fields:

    • status: The status of the application after the operation. You can get the status using the is_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 the is_errored method and get the error message using the get_error method.
    • outputs: A vector of outputs generated by the application. You can check the outputs vector using the get_outputs method and validate the outputs content.
    • metadata: A metadata object with additional information about the application advance (only available in the advance operation). To get the metadata object, use the get_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 result
    assert!(result.is_accepted(), "Expected Accept status");
    // Check if there are any errors
    /// in this case, we expect no errors because the application should be accepted
    assert!(!result.is_errored(), "Expected no error");
    // Validate the outputs length
    assert_eq!(
    result.get_outputs().len(),
    3,
    "Expected 3 outputs, got {}",
    result.get_outputs().len()
    );
    // Validate the outputs content
    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"
    );
    // Validate the sender address
    assert_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:

echo_tests.rs
#[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.

Terminal
cargo test

The output will show the results of each test, including the status and any errors encountered during the test execution.

Terminal
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 test
test 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");
}