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
- Unit Tests: Test individual functions
- Integration Tests: Test contract interactions
- Property Tests: Test invariants
- 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
RUSTuse 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!
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");
}