Smart Contract Testing

Writing comprehensive tests for smart contracts.

Advanced⏱️ 45 min📚 Prerequisites: 1

Smart Contract Testing

Testing is critical for smart contracts since bugs can lead to loss of funds. Rust's testing framework is excellent for contract testing.

Test Structure

RUST
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_transfer() {
        // Test transfer functionality
    }
}

Test Categories

  1. Unit Tests: Test individual functions
  2. Integration Tests: Test contract interactions
  3. Property Tests: Test invariants
  4. Fuzz Tests: Test with random inputs

Example Test Suite

RUST
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_initial_balance() {
        let contract = TokenContract::new("Token".to_string(), "TKN".to_string(), 1000);
        assert_eq!(contract.balance_of("deployer"), 1000);
    }
    
    #[test]
    fn test_transfer_success() {
        let mut contract = TokenContract::new("Token".to_string(), "TKN".to_string(), 1000);
        contract.transfer("deployer".to_string(), "alice".to_string(), 100).unwrap();
        assert_eq!(contract.balance_of("alice"), 100);
        assert_eq!(contract.balance_of("deployer"), 900);
    }
    
    #[test]
    fn test_transfer_insufficient_balance() {
        let mut contract = TokenContract::new("Token".to_string(), "TKN".to_string(), 100);
        let result = contract.transfer("deployer".to_string(), "alice".to_string(), 200);
        assert!(result.is_err());
    }
}

Test Best Practices

  • Cover edge cases: Zero, max values, boundaries
  • Test failures: Ensure errors are handled
  • Test invariants: Total supply should never change
  • Test access control: Unauthorized access should fail
  • Integration tests: Test with multiple contracts

Property-Based Testing

RUST
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_transfer_invariants(amount in 0..1000u64) {
        // Test that transfer maintains invariants
    }
}

Gas Testing

In blockchain, test gas consumption:

  • Optimize storage: Minimize storage operations
  • Batch operations: Combine multiple operations
  • Cache reads: Avoid repeated storage reads

Code Examples

Unit Tests

Writing unit tests for contracts

RUST
struct TokenContract {
    balances: std::collections::HashMap<String, u64>,
}
impl TokenContract {
    fn new() -> Self {
        let mut balances = std::collections::HashMap::new();
        balances.insert(String::from("deployer"), 1000);
        TokenContract { balances }
    }
    fn balance_of(&self, address: &str) -> u64 {
        self.balances.get(address).copied().unwrap_or(0)
    }
    fn transfer(&mut self, from: String, to: String, amount: u64) -> Result<(), String> {
        let balance = self.balance_of(&from);
        if balance < amount {
            return Err(String::from("Insufficient balance"));
        }
        *self.balances.entry(from).or_insert(0) -= amount;
        *self.balances.entry(to).or_insert(0) += amount;
        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_initial_balance() {
        let contract = TokenContract::new();
        assert_eq!(contract.balance_of("deployer"), 1000);
    }
    #[test]
    fn test_transfer() {
        let mut contract = TokenContract::new();
        contract.transfer(
            String::from("deployer"),
            String::from("alice"),
            100,
        ).unwrap();
        assert_eq!(contract.balance_of("alice"), 100);
        assert_eq!(contract.balance_of("deployer"), 900);
    }
    #[test]
    fn test_insufficient_balance() {
        let mut contract = TokenContract::new();
        let result = contract.transfer(
            String::from("deployer"),
            String::from("alice"),
            2000,
        );
        assert!(result.is_err());
    }
}
fn main() {
    println!("Run: cargo test");
}

Explanation:

Unit tests verify individual functions work correctly. They should test both success and failure cases to ensure robust contracts.

Testing Invariants

Testing that invariants are maintained

RUST
struct TokenContract {
    total_supply: u64,
    balances: std::collections::HashMap<String, u64>,
}
impl TokenContract {
    fn new(total_supply: u64) -> Self {
        let mut balances = std::collections::HashMap::new();
        balances.insert(String::from("deployer"), total_supply);
        TokenContract { total_supply, balances }
    }
    fn total_balance(&self) -> u64 {
        self.balances.values().sum()
    }
    fn transfer(&mut self, from: String, to: String, amount: u64) -> Result<(), String> {
        let balance = self.balances.get(&from).copied().unwrap_or(0);
        if balance < amount {
            return Err(String::from("Insufficient balance"));
        }
        *self.balances.entry(from).or_insert(0) -= amount;
        *self.balances.entry(to).or_insert(0) += amount;
        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_total_supply_invariant() {
        let mut contract = TokenContract::new(1000);
        assert_eq!(contract.total_balance(), contract.total_supply);
        contract.transfer(
            String::from("deployer"),
            String::from("alice"),
            100,
        ).unwrap();
        assert_eq!(contract.total_balance(), contract.total_supply);
    }
}
fn main() {
    println!("Test invariants with: cargo test");
}

Explanation:

Invariant testing ensures that important properties (like total supply) are always maintained, even after operations.

Exercises

Write Tests

Write tests for a simple contract!

Medium

Starter Code:

RUST
struct Contract {
    value: u64,
}
impl Contract {
    fn new() -> Self {
        Contract { value: 0 }
    }
    fn set_value(&mut self, v: u64) {
        self.value = v;
    }
    fn get_value(&self) -> u64 {
        self.value
    }
}
fn main() {
    println!("Run tests with: cargo test");
}