Testing smart contracts with Stylus
A comprehensive guide to writing and running tests for Stylus smart contracts.
Introduction
The Stylus SDK provides a robust testing framework that allows developers to write and run tests for their contracts directly in Rust without deploying to a blockchain. This guide will walk you through the process of writing and running tests for Stylus contracts using the built-in testing framework.
The Stylus testing framework allows you to:
- Simulate a complete Ethereum environment for your tests without the need for running a test node
- Test contract storage operations and state transitions
- Mock transaction context and block information
- Test contract-to-contract interactions with mocked calls
- Verify contract logic without deployment costs or delays
- Simulate various user scenarios and edge cases
Prerequisites
Before you begin, make sure you have:
- Basic familiarity with Rust and smart contract development
- Understanding of unit testing concepts
- Rust toolchain: follow the instructions on Rust Lang's installation page to install a complete Rust toolchain (v1.91 or newer) on your system. After installation, ensure you can access the programs
rustup,rustc, andcargofrom your preferred terminal application.
The Stylus Testing Framework
The Stylus SDK includes testing, a module that provides all the tools you need to test your contracts. This module includes:
- TestVM: A mock implementation of the Stylus VM that can simulate all host functions
- TestVMBuilder: A builder pattern to conveniently configure the test VM
- Built-in utilities for mocking calls, storage, and other EVM operations
Key Components
Here are the components you'll use when testing your Stylus contracts:
- TestVM: The core component that simulates the Stylus execution environment
- Storage accessors: For testing contract state changes
- Call mocking: For simulating interactions with other contracts
- Block context: For testing time-dependent logic
Example Smart Contract: Cupcake Vending Machine
Let's look at a Rust-based cupcake vending machine smart contract. This contract follows two simple rules:
- The vending machine will distribute a cupcake to anyone who hasn't received one in the last 5 seconds
- The vending machine tracks each user's cupcake balance
You can find all the code in this tutorial as a Rust workspace in the Quickstart repo
Writing Tests for the Vending Machine
Now, let's write tests for our vending machine contract using the Stylus testing framework. We'll create tests that verify:
- Users can get an initial cupcake
- Users must wait 5 seconds between cupcakes
- Cupcake balances are tracked correctly
- The contract state updates properly
Test Structure
Create a test file using standard Rust test patterns. Here's the basic structure:
// Import necessary dependencies
#[cfg(test)]
mod test {
use super::*;
use alloy_primitives::address;
use stylus_sdk::testing::*;
#[test]
fn test_give_cupcake_to() {
// Set up test environment
let vm = TestVM::default();
// Initialize your contract
let mut contract = VendingMachine::from(&vm);
// Test logic goes here...
}Using the TestVM
The TestVM simulates the execution environment for your contract, removing the need to run your tests against a test node.
The TestVM allows you to control aspects like:
- Block timestamp and number
- Account balances
- Transaction value and sender
- Storage state
Let's create a test suite that covers all aspects of our contract, we'll go over the code features one by one:
TestVM: advanced use
This test shows how you can use advanced configuration and usage of the TestVM by creating and configuring a TestVM with custom parameters:
- Setting blockchain state (timestamps, block numbers)
- Interacting with contract methods
- Taking and inspecting VM state snapshots
- Mocking external contract calls
- Testing time-dependent contract behavior
- Testing logs
Here is a cargo.toml file to add the required dependencies:
You can find the example above in the stylus-quickstart-vending-machine git repository.
Running Tests
Unit tests run with the standard Rust test command:
cargo testTo run a specific test:
cargo test test_give_cupcakeTesting Best Practices
-
Test Isolation
- Create a new
TestVMinstance for each test - Avoid relying on state from previous tests
- Create a new
-
Comprehensive Coverage
- Test both success and error conditions
- Test edge cases and boundary conditions
- Verify all public functions and important state transitions
-
Clear Assertions
- Use descriptive error messages in assertions
- Make assertions that verify the actual behavior you care about
-
Realistic Scenarios
- Test real-world usage patterns
- Include tests for authorization and access control
-
Gas and Resource Efficiency
- For complex contracts, consider testing gas usage patterns
- Look for storage optimization opportunities
Migrating from Global Accessors to VM Accessors
As of Stylus SDK 0.8.0, there's a shift away from global host function invocations to using the .vm() method. This is a safer approach that makes testing easier. For example:
// Old style (deprecated)
let timestamp = block::timestamp();
// New style (preferred)
let timestamp = self.vm().block_timestamp();To make your contracts more testable, make sure they access host methods through the HostAccess trait with the .vm() method.