Testing Contracts' Internals

Sometimes, you want to test a function which uses Starknet context (like block number, timestamp, storage access) without deploying the actual contract.

Since every test is treated like a contract, using the aforementioned pattern you can 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

To facilitate such use cases, we have a handful of utilities which make a test behave like a contract.

contract_state_for_testing() - State of Test Contract

This is a function generated by the #[starknet::contract] macro. It can be used to test some functions which accept the state as an argument, see the example below:

#[starknet::interface]
trait IContract<TContractState> {}

#[starknet::contract]
pub mod Contract {
    #[storage]
    pub struct Storage {
        pub balance: felt252,
    }

    #[generate_trait]
    pub impl InternalImpl of InternalTrait {
        fn internal_function(self: @ContractState) -> felt252 {
            self.balance.read()
        }
    }

    pub fn other_internal_function(self: @ContractState) -> felt252 {
        self.balance.read() + 5
    }
}

#[cfg(test)]
mod tests {
    use core::starknet::storage::{
        StoragePointerReadAccess, StoragePointerWriteAccess
    }; // <--- Ad. 1
    use super::Contract;
    use super::Contract::{InternalTrait, other_internal_function}; // <--- Ad. 2

    #[test]
    fn test_internal() {
        let mut state = Contract::contract_state_for_testing(); // <--- Ad. 3
        state.balance.write(10);

        let value = state.internal_function();
        assert(value == 10, 'Incorrect storage value');

        let other_value = other_internal_function(@state);
        assert(other_value == 15, 'Incorrect return value');
    }
}

This code contains some caveats:

  1. To access read/write methods of the state fields (in this case it's balance) you need to also import StoragePointerReadAccess and StoragePointerWriteAccess for reading and writing respectively.
  2. To access functions implemented directly on the state you need to also import an appropriate trait or function.
  3. This function will always return the struct keeping track of the state of the test. It means that within one test every result of contract_state_for_testing actually points to the same state.

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::result::ResultTrait;
use core::box::BoxTrait;
use starknet::ContractAddress;
use snforge_std::{start_cheat_block_number, stop_cheat_block_number, test_address};

#[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 {
    use core::result::ResultTrait;
    use starknet::ClassHash;

    #[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::{
    declare, ContractClassTrait, spy_events, EventSpy, EventSpyTrait, EventSpyAssertionsTrait,
    Event, 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 starknet::{ContractAddress, SyscallResultTrait, syscalls::emit_event_syscall};
use snforge_std::{
    declare, ContractClassTrait, spy_events, EventSpy, EventSpyTrait, EventSpyAssertionsTrait,
    Event, test_address
};

#[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 {
    #[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 testing_contract_internals::using_library_calls::{
    ILibraryContractSafeLibraryDispatcher, ILibraryContractSafeDispatcherTrait
};
use starknet::{ClassHash, ContractAddress, syscalls::library_call_syscall};
use snforge_std::{declare, DeclareResultTrait};

#[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.