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::storage::{ StoragePointerReadAccess, StorageMapReadAccess, StorageMapWriteAccess, Map, }; use starknet::ContractAddress; #[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 starknet::storage::{StorageMapReadAccess, StorageMapWriteAccess}; use testing_contract_internals::contract::{Contract, IContractDispatcher, IContractDispatcherTrait}; use testing_contract_internals::contract::Contract::InternalTrait; use snforge_std::{declare, DeclareResultTrait, ContractClassTrait, interact_with_state}; use starknet::ContractAddress; 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::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 { #[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 { use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess}; #[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.