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:
- To access
read/write
methods of the state fields (in this case it'sbalance
) you need to also importStoragePointerReadAccess
andStoragePointerWriteAccess
for reading and writing respectively. - To access functions implemented directly on the state you need to also import an appropriate trait or function.
- 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 throughcontract_state_for_testing
object and vice-versa.