Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Important: If you're upgrading snforge to version 0.48.0 or later, please read the 0.48.0 Migration Guide.

Testing Contracts' Internals

Sometimes, you want to test a function that uses Starknet context such as block number, timestamp or storage access.

Since every test is treated like a contract (with the address test_address), you can use the aforementioned pattern to test:

  • functions which are not available through the interface (but your contract uses them)
  • functions which are internal
  • functions performing specific operations on the contracts' storage or context data
  • library calls directly in the tests

Utilities For Testing Internals

contract_state_for_testing() - State of Test Contract

This function is generated by the #[starknet::contract] macro. It can be used to test functions that accept the state as an argument.

In the example below, we will use the following contract:

#[starknet::interface]
pub trait IContract<TContractState> {
    fn get_balance_at(self: @TContractState, address: starknet::ContractAddress) -> u64;
}

#[starknet::contract]
pub mod Contract {
    use starknet::ContractAddress;
    use starknet::storage::{
        Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
    };

    #[storage]
    pub struct Storage {
        pub balances: Map<ContractAddress, u64>,
    }

    #[abi(embed_v0)]
    impl ContractImpl of super::IContract<ContractState> {
        fn get_balance_at(self: @ContractState, address: ContractAddress) -> u64 {
            self.balances.read(address)
        }
    }

    #[generate_trait]
    pub impl InternalImpl of InternalTrait {
        fn _internal_set_balance(ref self: ContractState, address: ContractAddress, balance: u64) {
            self.balances.write(address, balance);
        }
    }
}

Modifying the state of an existing contract

There is a special interact_with_state cheatcode dedicated for using contract_state_for_testing with a deployed contract. By using this function, the state will be modified for the provided contract address.

// 0. Import necessary structs and traits
use snforge_std::{ContractClassTrait, DeclareResultTrait, declare, interact_with_state};
use starknet::ContractAddress;
use starknet::storage::{StorageMapReadAccess, StorageMapWriteAccess};
use testing_contract_internals::contract::Contract::InternalTrait;
use testing_contract_internals::contract::{Contract, IContractDispatcher, IContractDispatcherTrait};

fn deploy_contract() -> starknet::ContractAddress {
    let contract = declare("Contract").unwrap().contract_class();
    let (contract_address, _) = contract.deploy(@array![]).unwrap();
    contract_address
}

#[test]
fn test_storage() {
    // 1. Deploy your contract
    let contract_address = deploy_contract();
    let dispatcher = IContractDispatcher { contract_address };

    let contract_to_modify: ContractAddress = 0x123.try_into().unwrap();

    assert(dispatcher.get_balance_at(contract_to_modify) == 0, 'Wrong balance');

    // 2. Use `interact_with_state` to access and modify the contract's storage
    interact_with_state(
        contract_address,
        || {
            // 3. Get access to the contract's state
            let mut state = Contract::contract_state_for_testing();

            // 4. Read from storage
            let current_balance = state.balances.read(contract_to_modify);

            // 5. Write to storage
            state.balances.write(contract_to_modify, current_balance + 100);
        },
    );

    assert(dispatcher.get_balance_at(contract_to_modify) == 100, 'Wrong balance');
}

#[test]
fn test_internal_function() {
    // 1. Deploy your contract
    let contract_address = deploy_contract();
    let dispatcher = IContractDispatcher { contract_address };

    let contract_to_modify: ContractAddress = 0x456.try_into().unwrap();

    assert(dispatcher.get_balance_at(contract_to_modify) == 0, 'Wrong balance');

    // 2. Use `interact_with_state` to call contract's internal function
    interact_with_state(
        contract_address,
        || {
            // 3. Get access to the contract's state
            let mut state = Contract::contract_state_for_testing();

            // 4. Call internal function
            state._internal_set_balance(contract_to_modify, 200);
        },
    );

    assert(dispatcher.get_balance_at(contract_to_modify) == 200, 'Wrong balance');
}

⚠️ Warning

When using contract_state_for_testing without the interact_with_state cheatcode, the storage is modified in the context of the test_address contract. Therefore, it is not recommended to use contract_state_for_testing without the cheatcode, as it can lead to unexpected results.

snforge_std::test_address() - Address of Test Contract

That function returns the contract address of the test. It is useful, when you want to:

  • Mock the context (cheat_caller_address, cheat_block_timestamp, cheat_block_number, ...)
  • Spy for events emitted in the test

Example usages:

1. Mocking the context info

Example for cheat_block_number, same can be implemented for cheat_caller_address/cheat_block_timestamp/elect etc.

use core::box::BoxTrait;
use core::result::ResultTrait;
use snforge_std::{start_cheat_block_number, stop_cheat_block_number, test_address};
use starknet::ContractAddress;

#[test]
fn test_cheat_block_number_test_state() {
    let test_address: ContractAddress = test_address();
    let old_block_number = starknet::get_block_info().unbox().block_number;

    start_cheat_block_number(test_address, 234);
    let new_block_number = starknet::get_block_info().unbox().block_number;
    assert(new_block_number == 234, 'Wrong block number');

    stop_cheat_block_number(test_address);
    let new_block_number = starknet::get_block_info().unbox().block_number;
    assert(new_block_number == old_block_number, 'Block num did not change back');
}

2. Spying for events

You can use both starknet::emit_event_syscall, and the spies will capture the events, emitted in a #[test] function, if you pass the test_address() as a spy parameter (or spy on all events).

Given the emitting contract implementation:

#[starknet::interface]
pub trait IEmitter<TContractState> {
    fn emit_event(ref self: TContractState);
}

#[starknet::contract]
pub mod Emitter {
    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        ThingEmitted: ThingEmitted,
    }

    #[derive(Drop, starknet::Event)]
    pub struct ThingEmitted {
        pub thing: felt252,
    }

    #[storage]
    struct Storage {}

    #[external(v0)]
    pub fn emit_event(ref self: ContractState) {
        self.emit(Event::ThingEmitted(ThingEmitted { thing: 420 }));
    }
}

You can implement this test:

use core::array::ArrayTrait;
use snforge_std::{
    ContractClassTrait, Event, EventSpy, EventSpyAssertionsTrait, EventSpyTrait, declare,
    spy_events, test_address,
};
use testing_contract_internals::spying_for_events::Emitter;

#[test]
fn test_expect_event() {
    let contract_address = test_address();
    let mut spy = spy_events();

    let mut testing_state = Emitter::contract_state_for_testing();
    Emitter::emit_event(ref testing_state);

    spy
        .assert_emitted(
            @array![
                (
                    contract_address,
                    Emitter::Event::ThingEmitted(Emitter::ThingEmitted { thing: 420 }),
                ),
            ],
        )
}

You can also use the starknet::emit_event_syscall directly in the tests:

use core::array::ArrayTrait;
use core::result::ResultTrait;
use snforge_std::{
    ContractClassTrait, Event, EventSpy, EventSpyAssertionsTrait, EventSpyTrait, declare,
    spy_events, test_address,
};
use starknet::syscalls::emit_event_syscall;
use starknet::{ContractAddress, SyscallResultTrait};

#[test]
fn test_expect_event() {
    let contract_address = test_address();
    let mut spy = spy_events();

    emit_event_syscall(array![1234].span(), array![2345].span()).unwrap_syscall();

    spy
        .assert_emitted(
            @array![(contract_address, Event { keys: array![1234], data: array![2345] })],
        );

    assert(spy.get_events().events.len() == 1, 'There should no more events');
}

Using Library Calls With the Test State Context

Using the above utilities, you can avoid deploying a mock contract, to test a library_call with a LibraryCallDispatcher.

For contract implementation:

#[starknet::interface]
pub trait ILibraryContract<TContractState> {
    fn get_value(self: @TContractState) -> felt252;
    fn set_value(ref self: TContractState, number: felt252);
}

#[starknet::contract]
pub mod LibraryContract {
    use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
    #[storage]
    struct Storage {
        value: felt252,
    }

    #[external(v0)]
    pub fn get_value(self: @ContractState) -> felt252 {
        self.value.read()
    }

    #[external(v0)]
    pub fn set_value(ref self: ContractState, number: felt252) {
        self.value.write(number);
    }
}

We use the SafeLibraryDispatcher like this:

use snforge_std::{DeclareResultTrait, declare};
use starknet::syscalls::library_call_syscall;
use starknet::{ClassHash, ContractAddress};
use testing_contract_internals::using_library_calls::{
    ILibraryContractSafeDispatcherTrait, ILibraryContractSafeLibraryDispatcher,
};

#[test]
fn test_library_calls() {
    let class_hash = declare("LibraryContract").unwrap().contract_class().class_hash.clone();
    let lib_dispatcher = ILibraryContractSafeLibraryDispatcher { class_hash };

    let value = lib_dispatcher.get_value().unwrap();
    assert(value == 0, 'Incorrect state');

    lib_dispatcher.set_value(10).unwrap();

    let value = lib_dispatcher.get_value().unwrap();
    assert(value == 10, 'Incorrect state');
}

⚠️ Warning

This library call will write to the test_address memory segment, so it can potentially overwrite the changes you make to the memory through contract_state_for_testing object and vice-versa.