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::contract]
mod Contract {
    #[storage]
    struct Storage {
        balance: felt252, 
    }
    
    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn internal_function(self: @ContractState) -> felt252 {
            self.balance.read()
        }
    }

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

use Contract::balanceContractMemberStateTrait;   // <--- Ad. 1
use 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(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 <member_name>ContractMemberStateTrait from your contract, where <member_name> is the name of the storage variable inside Storage struct.
  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 (prank, warp, roll, spoof)
  • Spy for events emitted in the test

Example usages:

1. Mocking the context info

Example for roll, same can be implemented for prank/spoof/warp/elect etc.

use result::ResultTrait;
use box::BoxTrait;
use starknet::ContractAddress;
use snforge_std::{
    CheatTarget,
    start_roll, stop_roll,
    test_address
};

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

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

    stop_roll(CheatTarget::One(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::contract]
mod Emitter {
    use result::ResultTrait;
    use starknet::ClassHash;
    
    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        ThingEmitted: ThingEmitted
    }
    
    #[derive(Drop, starknet::Event)]
    struct ThingEmitted {
        thing: felt252
    }

    #[storage]
    struct Storage {}

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

You can implement this test:

use array::ArrayTrait;
use snforge_std::{ 
    declare, ContractClassTrait, spy_events, 
    EventSpy, EventFetcher, 
    EventAssertions, Event, SpyOn, test_address 
};
#[test]
fn test_expect_event() {
    let contract_address = test_address();
    let mut spy = spy_events(SpyOn::One(contract_address));
    
    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 array::ArrayTrait;
use result::ResultTrait;
use starknet::SyscallResultTrait;
use starknet::ContractAddress;
use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventFetcher,
    event_name_hash, EventAssertions, Event, SpyOn, test_address };

#[test]
fn test_expect_events_simple() {
    let test_address = test_address();
    let mut spy = spy_events(SpyOn::One(test_address));
    assert(spy._id == 0, 'Id should be 0');

    starknet::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.events.len() == 0, 'There should be no events left');
}

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::contract]
mod LibraryContract {
    use result::ResultTrait;
    use starknet::ClassHash;
    use starknet::library_call_syscall;

    #[storage]
    struct Storage {
        value: felt252
    }

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

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

We use the SafeLibraryDispatcher like this:

use result::ResultTrait;
use starknet::{ ClassHash, library_call_syscall, ContractAddress };
use snforge_std::{ declare };

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

    fn set_value(
        ref self: TContractState,
        number: felt252
    );
}

#[test]
fn test_library_calls() {
    let class_hash = declare("LibraryContract").class_hash;
    let lib_dispatcher = ILibraryContractSafeLibraryDispatcher { class_hash };
    let value = lib_dispatcher.get_value().unwrap();
    assert(value == 0, 'Incorrect state');
    lib_dispatcher.set_value(10);
    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.