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 theinteract_with_state
cheatcode, the storage is modified in the context of thetest_address
contract. Therefore, it is not recommended to usecontract_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 throughcontract_state_for_testing
object and vice-versa.