Testing events
Examples are based on the following SpyEventsChecker contract implementation:
#[starknet::interface]
pub trait ISpyEventsChecker<TContractState> {
fn emit_one_event(ref self: TContractState, some_data: felt252);
}
#[starknet::contract]
pub mod SpyEventsChecker {
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
FirstEvent: FirstEvent,
}
#[derive(Drop, starknet::Event)]
pub struct FirstEvent {
pub some_data: felt252,
}
#[external(v0)]
pub fn emit_one_event(ref self: ContractState, some_data: felt252) {
self.emit(FirstEvent { some_data });
}
}
Asserting emission with assert_emitted method
This is the simpler way, in which you don't have to fetch the events explicitly. See the below code for reference:
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, spy_events,
EventSpyAssertionsTrait // Add for assertions on the EventSpy
};
use testing_events::contract::{
ISpyEventsCheckerDispatcher, ISpyEventsCheckerDispatcherTrait, SpyEventsChecker,
};
#[test]
fn test_simple_assertions() {
let contract = declare("SpyEventsChecker").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ISpyEventsCheckerDispatcher { contract_address };
let mut spy = spy_events(); // Ad. 1
dispatcher.emit_one_event(123);
spy
.assert_emitted(
@array![ // Ad. 2
(
contract_address,
SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 123 },
),
),
],
);
}
Let's go through the code:
- After contract deployment, we created the spy using
spy_eventscheatcode. From this moment all emitted events will be spied. - Asserting is done using the
assert_emittedmethod. It takes an array snapshot of(ContractAddress, event)tuples we expect that were emitted.
📝 Note We can pass events defined in the contract and construct them like in the
self.emitmethod!
Asserting lack of event emission with assert_not_emitted
In cases where you want to test an event was not emitted, use the assert_not_emitted function.
It works similarly as assert_emitted with the only difference that it panics if an event was emitted during the execution.
Given the example above, we can check that a different FirstEvent was not emitted:
spy.assert_not_emitted(@array![
(
contract_address,
SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 456 }
)
)
]);
Note that both the event name and event data are checked.
If a function emitted an event with the same name but a different payload, the assert_not_emitted function will pass.
Asserting the events manually
If you wish to assert the data manually, you can do that on the Events structure.
Simply call get_events() on your EventSpy and access events field on the returned Events value.
Then, you can access the events and assert data by yourself.
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, spy_events, EventSpyAssertionsTrait,
EventSpyTrait, // Add for fetching events directly
Event, // A structure describing a raw `Event`
IsEmitted // Trait for checking if a given event was ever emitted
};
use starknet::ContractAddress;
use testing_events::contract::{
ISpyEventsCheckerDispatcher, ISpyEventsCheckerDispatcherTrait, SpyEventsChecker,
};
#[test]
fn test_complex_assertions() {
let contract = declare("SpyEventsChecker").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ISpyEventsCheckerDispatcher { contract_address };
let mut spy = spy_events(); // Ad 1.
dispatcher.emit_one_event(123);
let events = spy.get_events(); // Ad 2.
assert(events.events.len() == 1, 'There should be one event');
let expected_event = SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 123 },
);
assert!(events.is_emitted(contract_address, @expected_event)); // Ad 3.
let expected_events: Array<(ContractAddress, Event)> = array![
(contract_address, expected_event.into()),
];
assert!(events.events == expected_events); // Ad 4.
let (from, event) = events.events.at(0); // Ad 5.
assert(from == @contract_address, 'Emitted from wrong address');
assert(event.keys.len() == 1, 'There should be one key');
assert(event.keys.at(0) == @selector!("FirstEvent"), 'Wrong event name'); // Ad 6.
assert(event.data.len() == 1, 'There should be one data');
}
Let's go through important parts of the provided code:
- After contract deployment we created the spy with
spy_eventscheatcode. From this moment all events emitted by theSpyEventsCheckercontract will be spied. - We have to call
get_eventsmethod on the created spy to fetch our events and get theEventsstructure. - We can check if certain event was emitted.
- Emitted events can be explicitly compared.
- To get our particular event, we need to access the
eventsproperty and get the event under an index. Sinceeventsis an array holding a tuple ofContractAddressandEvent, we unpack it usinglet (from, event). - If the event is emitted by calling
self.emitmethod, its hashed name is saved under thekeys.at(0)(this way Starknet handles events)
📝 Note To assert the
nameproperty we have to hash a string with theselector!macro.
Filtering Events
Sometimes, when you assert the events manually, you might not want to get all the events, but only ones from
a particular address. You can address that by using the method emitted_by on the Events structure.
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, spy_events, EventSpyAssertionsTrait,
EventSpyTrait, Event,
EventsFilterTrait // Add for filtering the Events object (result of `get_events`)
};
use testing_events::contract::{
ISpyEventsCheckerDispatcher, ISpyEventsCheckerDispatcherTrait, SpyEventsChecker,
};
#[test]
fn test_assertions_with_filtering() {
let contract = declare("SpyEventsChecker").unwrap().contract_class();
let (first_address, _) = contract.deploy(@array![]).unwrap();
let (second_address, _) = contract.deploy(@array![]).unwrap();
let first_dispatcher = ISpyEventsCheckerDispatcher { contract_address: first_address };
let second_dispatcher = ISpyEventsCheckerDispatcher { contract_address: second_address };
let mut spy = spy_events();
first_dispatcher.emit_one_event(123);
second_dispatcher.emit_one_event(234);
second_dispatcher.emit_one_event(345);
let events_from_first_address = spy.get_events().emitted_by(first_address);
let events_from_second_address = spy.get_events().emitted_by(second_address);
let (from_first, event_from_first) = events_from_first_address.events.at(0);
assert(from_first == @first_address, 'Emitted from wrong address');
assert(event_from_first.data.at(0) == @123.into(), 'Data should be 123');
let (from_second_one, event_from_second_one) = events_from_second_address.events.at(0);
assert(from_second_one == @second_address, 'Emitted from wrong address');
assert(event_from_second_one.data.at(0) == @234.into(), 'Data should be 234');
let (from_second_two, event_from_second_two) = events_from_second_address.events.at(1);
assert(from_second_two == @second_address, 'Emitted from wrong address');
assert(event_from_second_two.data.at(0) == @345.into(), 'Data should be 345');
}
events_from_first_address has events emitted by the first contract only.
Similarly, events_from_second_address has events emitted by the second contract.
Asserting Events Emitted With emit_event_syscall
Events emitted with emit_event_syscall could have nonstandard (not defined anywhere) keys and data.
They can also be asserted with spy.assert_emitted method.
Let's extend our SpyEventsChecker with emit_event_with_syscall method:
#[starknet::interface]
pub trait ISpySyscallEventsChecker<TContractState> {
fn emit_one_event(ref self: TContractState, some_data: felt252);
fn emit_event_with_syscall(ref self: TContractState, some_key: felt252, some_data: felt252);
}
#[starknet::contract]
pub mod SpySyscallEventsChecker {
// ...
// Rest of the implementation identical to `SpyEventsChecker`
use core::starknet::SyscallResultTrait;
use core::starknet::syscalls::emit_event_syscall;
#[external(v0)]
pub fn emit_event_with_syscall(ref self: ContractState, some_key: felt252, some_data: felt252) {
emit_event_syscall(array![some_key].span(), array![some_data].span()).unwrap_syscall();
}
}
And add a test for it:
use snforge_std::{
ContractClassTrait, DeclareResultTrait, Event, EventSpyAssertionsTrait, EventSpyTrait,
EventsFilterTrait, declare, spy_events,
};
use testing_events::syscall::{
ISpySyscallEventsCheckerDispatcher, ISpySyscallEventsCheckerDispatcherTrait,
};
#[test]
fn test_nonstandard_events() {
let contract = declare("SpySyscallEventsChecker").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ISpySyscallEventsCheckerDispatcher { contract_address };
let mut spy = spy_events();
dispatcher.emit_event_with_syscall(123, 456);
spy.assert_emitted(@array![(contract_address, Event { keys: array![123], data: array![456] })]);
}
Using Event struct from the snforge_std library we can easily assert nonstandard events.
This also allows for testing the events you don't have the code of, or you don't want to import those.
Events and ABI Stability
The testing approach where event structs from the contract are used may lead to accidental breaking changes in the contracts ABI.
For example, removing a #[key] attribute from this event would not lead to any code changes in the event tests.
#[derive(Drop, starknet::Event)]
pub struct FirstEvent {
#[key]
pub some_key: felt252,
pub some_data: felt252,
}
A recommended solution to this problem is using the base snforge_std::Event struct in tests.
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, spy_events,
EventSpyAssertionsTrait, // Add for assertions on the EventSpy
Event // Import the base Event
};
use testing_events::contract::{
ISpyEventsCheckerDispatcher, ISpyEventsCheckerDispatcherTrait, SpyEventsChecker,
};
#[test]
fn test_simple_assertions() {
let contract = declare("SpyEventsChecker").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ISpyEventsCheckerDispatcher { contract_address };
let mut spy = spy_events();
dispatcher.emit_one_event(123);
let mut keys = array![];
keys.append(selector!("FirstEvent")); // Append the name of the event to keys
let mut data = array![];
data.append(123); // Append the expected data
let expected = Event { keys, data }; // Instantiate the Event
spy.assert_emitted(@array![ // Assert
(contract_address, expected)]);
}