Fundamentals

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, and cargo from 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:

  1. The vending machine will distribute a cupcake to anyone who hasn't received one in the last 5 seconds
  2. The vending machine tracks each user's cupcake balance
Note

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:

  1. Users can get an initial cupcake
  2. Users must wait 5 seconds between cupcakes
  3. Cupcake balances are tracked correctly
  4. 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 test

To run a specific test:

cargo test test_give_cupcake

Testing Best Practices

  1. Test Isolation

    • Create a new TestVM instance for each test
    • Avoid relying on state from previous tests
  2. Comprehensive Coverage

    • Test both success and error conditions
    • Test edge cases and boundary conditions
    • Verify all public functions and important state transitions
  3. Clear Assertions

    • Use descriptive error messages in assertions
    • Make assertions that verify the actual behavior you care about
  4. Realistic Scenarios

    • Test real-world usage patterns
    • Include tests for authorization and access control
  5. 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.

On this page