Testing Smart Contracts
Testing smart contracts is a critical part of the development process. It is important to ensure that smart contracts behave as expected and that they are secure.
In a previous section of the Cairo Book, we learned how to write and structure our tests for Cairo programs. We demonstrated how these tests could be run using the scarb
command-line tool. While this approach is useful for testing standalone Cairo programs and functions, it lacks functionality for testing smart contracts that require control over the contract state and execution context. Therefore, in this section, we will introduce how to use Starknet Foundry, a smart contract development toolchain for Starknet, to test your Cairo contracts.
Throughout this chapter, we will be using as an example the PizzaFactory
contract in Listing 17-1 to demonstrate how to write tests with Starknet Foundry.
use core::starknet::ContractAddress;
#[starknet::interface]
pub trait IPizzaFactory<TContractState> {
fn increase_pepperoni(ref self: TContractState, amount: u32);
fn increase_pineapple(ref self: TContractState, amount: u32);
fn get_owner(self: @TContractState) -> ContractAddress;
fn change_owner(ref self: TContractState, new_owner: ContractAddress);
fn make_pizza(ref self: TContractState);
fn count_pizza(self: @TContractState) -> u32;
}
#[starknet::contract]
pub mod PizzaFactory {
use super::IPizzaFactory;
use core::starknet::{ContractAddress, get_caller_address};
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
pepperoni: u32,
pineapple: u32,
pub owner: ContractAddress,
pizzas: u32,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
self.pepperoni.write(10);
self.pineapple.write(10);
self.owner.write(owner);
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
PizzaEmission: PizzaEmission,
}
#[derive(Drop, starknet::Event)]
pub struct PizzaEmission {
pub counter: u32,
}
#[abi(embed_v0)]
impl PizzaFactoryimpl of super::IPizzaFactory<ContractState> {
fn increase_pepperoni(ref self: ContractState, amount: u32) {
assert!(amount != 0, "Amount cannot be 0");
self.pepperoni.write(self.pepperoni.read() + amount);
}
fn increase_pineapple(ref self: ContractState, amount: u32) {
assert!(amount != 0, "Amount cannot be 0");
self.pineapple.write(self.pineapple.read() + amount);
}
fn make_pizza(ref self: ContractState) {
assert!(self.pepperoni.read() > 0, "Not enough pepperoni");
assert!(self.pineapple.read() > 0, "Not enough pineapple");
let caller: ContractAddress = get_caller_address();
let owner: ContractAddress = self.get_owner();
assert!(caller == owner, "Only the owner can make pizza");
self.pepperoni.write(self.pepperoni.read() - 1);
self.pineapple.write(self.pineapple.read() - 1);
self.pizzas.write(self.pizzas.read() + 1);
self.emit(PizzaEmission { counter: self.pizzas.read() });
}
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
fn change_owner(ref self: ContractState, new_owner: ContractAddress) {
self.set_owner(new_owner);
}
fn count_pizza(self: @ContractState) -> u32 {
self.pizzas.read()
}
}
#[generate_trait]
pub impl InternalImpl of InternalTrait {
fn set_owner(ref self: ContractState, new_owner: ContractAddress) {
let caller: ContractAddress = get_caller_address();
assert!(caller == self.get_owner(), "Only the owner can set ownership");
self.owner.write(new_owner);
}
}
}
Configuring your Scarb project with Starknet Foundry
The settings of your Scarb project can be configured in the Scarb.toml
file. To use Starknet Foundry as your testing tool, you will need to add it as a dev dependency in your Scarb.toml
file. At the time of writing, the latest version of Starknet Foundry is v0.22.0
- but you should use the latest version.
[dev-dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.33.0" }
[scripts]
test = "snforge test"
The scarb test
command is configured to execute scarb cairo-test
by default. In our settings, we have configured it to execute snforge test
instead. This will allow us to run our tests using Starknet Foundry when we run the scarb test
command.
Once your project is configured, you will need to install Starknet Foundry by following the installation guide from the Starknet Foundry Documentation. As usual, we recommend to use asdf
to manage versions of your development tools.
Testing Smart Contracts with Starknet Foundry
The usual command to run your tests using Starknet Foundry is snforge test
. However, when we configured our projects, we defined that the scarb test
command will run the snforge test
command. Therefore, during the rest of this chapter, consider that the scarb test
command will be using snforge test
under the hood.
The usual testing flow of a contract is as follows:
- Declare the class of the contract to test, identified by its name
- Serialize the constructor calldata into an array
- Deploy the contract and retrieve its address
- Interact with the contract's entrypoint to test various scenarios
Deploying the Contract to Test
In Listing 17-2, we wrote a function that deploys the PizzaFactory
contract and sets up the dispatcher for interactions.
use crate::pizza::{
IPizzaFactoryDispatcher, IPizzaFactoryDispatcherTrait, PizzaFactory,
PizzaFactory::{Event as PizzaEvents, PizzaEmission},
};
use crate::pizza::PizzaFactory::{InternalTrait};
use core::starknet::{ContractAddress, contract_address_const};
use core::starknet::storage::StoragePointerReadAccess;
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address, EventSpyAssertionsTrait, spy_events, load,
};
fn owner() -> ContractAddress {
contract_address_const::<'owner'>()
}
fn deploy_pizza_factory() -> (IPizzaFactoryDispatcher, ContractAddress) {
let contract = declare("PizzaFactory").unwrap().contract_class();
let owner: ContractAddress = contract_address_const::<'owner'>();
let constructor_calldata = array![owner.into()];
let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
let dispatcher = IPizzaFactoryDispatcher { contract_address };
(dispatcher, contract_address)
}
#[test]
fn test_constructor() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let pepperoni_count = load(pizza_factory_address, selector!("pepperoni"), 1);
let pineapple_count = load(pizza_factory_address, selector!("pineapple"), 1);
assert_eq!(pepperoni_count, array![10]);
assert_eq!(pineapple_count, array![10]);
assert_eq!(pizza_factory.get_owner(), owner());
}
#[test]
fn test_change_owner_should_change_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let new_owner: ContractAddress = contract_address_const::<'new_owner'>();
assert_eq!(pizza_factory.get_owner(), owner());
start_cheat_caller_address(pizza_factory_address, owner());
pizza_factory.change_owner(new_owner);
assert_eq!(pizza_factory.get_owner(), new_owner);
}
#[test]
#[should_panic(expected: "Only the owner can set ownership")]
fn test_change_owner_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.change_owner(not_owner);
stop_cheat_caller_address(pizza_factory_address);
}
#[test]
#[should_panic(expected: "Only the owner can make pizza")]
fn test_make_pizza_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.make_pizza();
}
#[test]
fn test_make_pizza_should_increment_pizza_counter() {
// Setup
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
start_cheat_caller_address(pizza_factory_address, owner());
let mut spy = spy_events();
// When
pizza_factory.make_pizza();
// Then
let expected_event = PizzaEvents::PizzaEmission(PizzaEmission { counter: 1 });
assert_eq!(pizza_factory.count_pizza(), 1);
spy.assert_emitted(@array![(pizza_factory_address, expected_event)]);
}
#[test]
fn test_set_as_new_owner_direct() {
let mut state = PizzaFactory::contract_state_for_testing();
let owner: ContractAddress = contract_address_const::<'owner'>();
state.set_owner(owner);
assert_eq!(state.owner.read(), owner);
}
Testing our Contract
Determining the behavior that your contract should respect is the first step in writing tests. In the PizzaFactory
contract, we determined that the contract should have the following behavior:
- Upon deployment, the contract owner should be set to the address provided in the constructor, and the factory should have 10 units of pepperoni and pineapple, and no pizzas created.
- If someone tries to make a pizza and they are not the owner, the operation should fail. Otherwise, the pizza count should be incremented, and an event should be emitted.
- If someone tries to take ownership of the contract and they are not the owner, the operation should fail. Otherwise, the owner should be updated.
Accessing Storage Variables with load
use crate::pizza::{
IPizzaFactoryDispatcher, IPizzaFactoryDispatcherTrait, PizzaFactory,
PizzaFactory::{Event as PizzaEvents, PizzaEmission},
};
use crate::pizza::PizzaFactory::{InternalTrait};
use core::starknet::{ContractAddress, contract_address_const};
use core::starknet::storage::StoragePointerReadAccess;
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address, EventSpyAssertionsTrait, spy_events, load,
};
fn owner() -> ContractAddress {
contract_address_const::<'owner'>()
}
fn deploy_pizza_factory() -> (IPizzaFactoryDispatcher, ContractAddress) {
let contract = declare("PizzaFactory").unwrap().contract_class();
let owner: ContractAddress = contract_address_const::<'owner'>();
let constructor_calldata = array![owner.into()];
let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
let dispatcher = IPizzaFactoryDispatcher { contract_address };
(dispatcher, contract_address)
}
#[test]
fn test_constructor() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let pepperoni_count = load(pizza_factory_address, selector!("pepperoni"), 1);
let pineapple_count = load(pizza_factory_address, selector!("pineapple"), 1);
assert_eq!(pepperoni_count, array![10]);
assert_eq!(pineapple_count, array![10]);
assert_eq!(pizza_factory.get_owner(), owner());
}
#[test]
fn test_change_owner_should_change_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let new_owner: ContractAddress = contract_address_const::<'new_owner'>();
assert_eq!(pizza_factory.get_owner(), owner());
start_cheat_caller_address(pizza_factory_address, owner());
pizza_factory.change_owner(new_owner);
assert_eq!(pizza_factory.get_owner(), new_owner);
}
#[test]
#[should_panic(expected: "Only the owner can set ownership")]
fn test_change_owner_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.change_owner(not_owner);
stop_cheat_caller_address(pizza_factory_address);
}
#[test]
#[should_panic(expected: "Only the owner can make pizza")]
fn test_make_pizza_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.make_pizza();
}
#[test]
fn test_make_pizza_should_increment_pizza_counter() {
// Setup
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
start_cheat_caller_address(pizza_factory_address, owner());
let mut spy = spy_events();
// When
pizza_factory.make_pizza();
// Then
let expected_event = PizzaEvents::PizzaEmission(PizzaEmission { counter: 1 });
assert_eq!(pizza_factory.count_pizza(), 1);
spy.assert_emitted(@array![(pizza_factory_address, expected_event)]);
}
#[test]
fn test_set_as_new_owner_direct() {
let mut state = PizzaFactory::contract_state_for_testing();
let owner: ContractAddress = contract_address_const::<'owner'>();
state.set_owner(owner);
assert_eq!(state.owner.read(), owner);
}
Once our contract is deployed, we want to assert that the initial values are set as expected. If our contract has an entrypoint that returns the value of a storage variable, we can call this entrypoint. Otherwise, we can use the load
function from snforge
to load the value of a storage variable inside our contract, even if not exposed by an entrypoint.
Mocking the Caller Address with start_cheat_caller_address
The security of our factory relies on the owner being the only one able to make pizzas and transfer ownership. To test this, we can use the start_cheat_caller_address
function to mock the caller address and assert that the contract behaves as expected.
use crate::pizza::{
IPizzaFactoryDispatcher, IPizzaFactoryDispatcherTrait, PizzaFactory,
PizzaFactory::{Event as PizzaEvents, PizzaEmission},
};
use crate::pizza::PizzaFactory::{InternalTrait};
use core::starknet::{ContractAddress, contract_address_const};
use core::starknet::storage::StoragePointerReadAccess;
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address, EventSpyAssertionsTrait, spy_events, load,
};
fn owner() -> ContractAddress {
contract_address_const::<'owner'>()
}
fn deploy_pizza_factory() -> (IPizzaFactoryDispatcher, ContractAddress) {
let contract = declare("PizzaFactory").unwrap().contract_class();
let owner: ContractAddress = contract_address_const::<'owner'>();
let constructor_calldata = array![owner.into()];
let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
let dispatcher = IPizzaFactoryDispatcher { contract_address };
(dispatcher, contract_address)
}
#[test]
fn test_constructor() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let pepperoni_count = load(pizza_factory_address, selector!("pepperoni"), 1);
let pineapple_count = load(pizza_factory_address, selector!("pineapple"), 1);
assert_eq!(pepperoni_count, array![10]);
assert_eq!(pineapple_count, array![10]);
assert_eq!(pizza_factory.get_owner(), owner());
}
#[test]
fn test_change_owner_should_change_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let new_owner: ContractAddress = contract_address_const::<'new_owner'>();
assert_eq!(pizza_factory.get_owner(), owner());
start_cheat_caller_address(pizza_factory_address, owner());
pizza_factory.change_owner(new_owner);
assert_eq!(pizza_factory.get_owner(), new_owner);
}
#[test]
#[should_panic(expected: "Only the owner can set ownership")]
fn test_change_owner_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.change_owner(not_owner);
stop_cheat_caller_address(pizza_factory_address);
}
#[test]
#[should_panic(expected: "Only the owner can make pizza")]
fn test_make_pizza_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.make_pizza();
}
#[test]
fn test_make_pizza_should_increment_pizza_counter() {
// Setup
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
start_cheat_caller_address(pizza_factory_address, owner());
let mut spy = spy_events();
// When
pizza_factory.make_pizza();
// Then
let expected_event = PizzaEvents::PizzaEmission(PizzaEmission { counter: 1 });
assert_eq!(pizza_factory.count_pizza(), 1);
spy.assert_emitted(@array![(pizza_factory_address, expected_event)]);
}
#[test]
fn test_set_as_new_owner_direct() {
let mut state = PizzaFactory::contract_state_for_testing();
let owner: ContractAddress = contract_address_const::<'owner'>();
state.set_owner(owner);
assert_eq!(state.owner.read(), owner);
}
Using start_cheat_caller_address
, we call the change_owner
function first as the owner, and then as a different address. We assert that the operation fails when the caller is not the owner, and that the owner is updated when the caller is the owner.
Capturing Events with spy_events
When a pizza is created, the contract emits an event. To test this, we can use the spy_events
function to capture the emitted events and assert that the event was emitted with the expected parameters. Naturally, we can also assert that the pizza count was incremented, and that only the owner can make a pizza.
use crate::pizza::{
IPizzaFactoryDispatcher, IPizzaFactoryDispatcherTrait, PizzaFactory,
PizzaFactory::{Event as PizzaEvents, PizzaEmission},
};
use crate::pizza::PizzaFactory::{InternalTrait};
use core::starknet::{ContractAddress, contract_address_const};
use core::starknet::storage::StoragePointerReadAccess;
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address, EventSpyAssertionsTrait, spy_events, load,
};
fn owner() -> ContractAddress {
contract_address_const::<'owner'>()
}
fn deploy_pizza_factory() -> (IPizzaFactoryDispatcher, ContractAddress) {
let contract = declare("PizzaFactory").unwrap().contract_class();
let owner: ContractAddress = contract_address_const::<'owner'>();
let constructor_calldata = array![owner.into()];
let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
let dispatcher = IPizzaFactoryDispatcher { contract_address };
(dispatcher, contract_address)
}
#[test]
fn test_constructor() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let pepperoni_count = load(pizza_factory_address, selector!("pepperoni"), 1);
let pineapple_count = load(pizza_factory_address, selector!("pineapple"), 1);
assert_eq!(pepperoni_count, array![10]);
assert_eq!(pineapple_count, array![10]);
assert_eq!(pizza_factory.get_owner(), owner());
}
#[test]
fn test_change_owner_should_change_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let new_owner: ContractAddress = contract_address_const::<'new_owner'>();
assert_eq!(pizza_factory.get_owner(), owner());
start_cheat_caller_address(pizza_factory_address, owner());
pizza_factory.change_owner(new_owner);
assert_eq!(pizza_factory.get_owner(), new_owner);
}
#[test]
#[should_panic(expected: "Only the owner can set ownership")]
fn test_change_owner_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.change_owner(not_owner);
stop_cheat_caller_address(pizza_factory_address);
}
#[test]
#[should_panic(expected: "Only the owner can make pizza")]
fn test_make_pizza_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.make_pizza();
}
#[test]
fn test_make_pizza_should_increment_pizza_counter() {
// Setup
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
start_cheat_caller_address(pizza_factory_address, owner());
let mut spy = spy_events();
// When
pizza_factory.make_pizza();
// Then
let expected_event = PizzaEvents::PizzaEmission(PizzaEmission { counter: 1 });
assert_eq!(pizza_factory.count_pizza(), 1);
spy.assert_emitted(@array![(pizza_factory_address, expected_event)]);
}
#[test]
fn test_set_as_new_owner_direct() {
let mut state = PizzaFactory::contract_state_for_testing();
let owner: ContractAddress = contract_address_const::<'owner'>();
state.set_owner(owner);
assert_eq!(state.owner.read(), owner);
}
Accessing Internal Functions with contract_state_for_testing
All the tests we have seen so far have been using a workflow that involves deploying the contract and interacting with the contract's entrypoints. However, sometimes we may want to test the internals of the contract directly, without deploying the contract. How could this be done, if we were reasoning in purely Cairo terms?
Recall the struct ContractState
, which is used as a parameter to all the entrypoints of a contract. To make it short, this struct contains zero-sized fields, corresponding to the storage variables of the contract. The only purpose of these fields is to allow the Cairo compiler to generate the correct code for accessing the storage variables. If we could create an instance of this struct, we could access these storage variables directly, without deploying the contract...
...and this is exactly what the contract_state_for_testing
function does! It creates an instance of the ContractState
struct, allowing us to call any function that takes as parameter a ContractState
struct, without deploying the contract. To interact with the storage variables properly, we need to manually import the traits that define access to the storage variables.
use crate::pizza::{
IPizzaFactoryDispatcher, IPizzaFactoryDispatcherTrait, PizzaFactory,
PizzaFactory::{Event as PizzaEvents, PizzaEmission},
};
use crate::pizza::PizzaFactory::{InternalTrait};
use core::starknet::{ContractAddress, contract_address_const};
use core::starknet::storage::StoragePointerReadAccess;
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address, EventSpyAssertionsTrait, spy_events, load,
};
fn owner() -> ContractAddress {
contract_address_const::<'owner'>()
}
fn deploy_pizza_factory() -> (IPizzaFactoryDispatcher, ContractAddress) {
let contract = declare("PizzaFactory").unwrap().contract_class();
let owner: ContractAddress = contract_address_const::<'owner'>();
let constructor_calldata = array![owner.into()];
let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
let dispatcher = IPizzaFactoryDispatcher { contract_address };
(dispatcher, contract_address)
}
#[test]
fn test_constructor() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let pepperoni_count = load(pizza_factory_address, selector!("pepperoni"), 1);
let pineapple_count = load(pizza_factory_address, selector!("pineapple"), 1);
assert_eq!(pepperoni_count, array![10]);
assert_eq!(pineapple_count, array![10]);
assert_eq!(pizza_factory.get_owner(), owner());
}
#[test]
fn test_change_owner_should_change_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let new_owner: ContractAddress = contract_address_const::<'new_owner'>();
assert_eq!(pizza_factory.get_owner(), owner());
start_cheat_caller_address(pizza_factory_address, owner());
pizza_factory.change_owner(new_owner);
assert_eq!(pizza_factory.get_owner(), new_owner);
}
#[test]
#[should_panic(expected: "Only the owner can set ownership")]
fn test_change_owner_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.change_owner(not_owner);
stop_cheat_caller_address(pizza_factory_address);
}
#[test]
#[should_panic(expected: "Only the owner can make pizza")]
fn test_make_pizza_should_panic_when_not_owner() {
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
let not_owner = contract_address_const::<'not_owner'>();
start_cheat_caller_address(pizza_factory_address, not_owner);
pizza_factory.make_pizza();
}
#[test]
fn test_make_pizza_should_increment_pizza_counter() {
// Setup
let (pizza_factory, pizza_factory_address) = deploy_pizza_factory();
start_cheat_caller_address(pizza_factory_address, owner());
let mut spy = spy_events();
// When
pizza_factory.make_pizza();
// Then
let expected_event = PizzaEvents::PizzaEmission(PizzaEmission { counter: 1 });
assert_eq!(pizza_factory.count_pizza(), 1);
spy.assert_emitted(@array![(pizza_factory_address, expected_event)]);
}
#[test]
fn test_set_as_new_owner_direct() {
let mut state = PizzaFactory::contract_state_for_testing();
let owner: ContractAddress = contract_address_const::<'owner'>();
state.set_owner(owner);
assert_eq!(state.owner.read(), owner);
}
These imports give us access to our internal functions (notably, set_owner
), as well as the read/write access to the owner
storage variable. Once we have these, we can interact with the contract directly, changing the address of the owner by calling the set_owner
method, accessible through InternalTrait
, and reading the owner
storage variable.
Note: Both approaches cannot be used at the same time. If you decide to deploy the contract, you interact with it using the dispatcher. If you decide to test the internal functions, you interact with the
ContractState
object directly.
$ scarb test
Running test listing_02_pizza_factory_snfoundry (snforge test)
Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
Finished `release` profile [optimized] target(s) in 0.99s
Compiling test(listings/ch17-starknet-smart-contracts-security/listing_02_pizza_factory_snfoundry/Scarb.toml)
Finished `dev` profile target(s) in 7 seconds
Collected 6 test(s) from listing_02_pizza_factory_snfoundry package
Running 6 test(s) from src/
[PASS] listing_02_pizza_factory_snfoundry::tests::foundry_test::test_set_as_new_owner_direct (gas: ~130)
[PASS] listing_02_pizza_factory_snfoundry::tests::foundry_test::test_change_owner_should_panic_when_not_owner (gas: ~298)
[PASS] listing_02_pizza_factory_snfoundry::tests::foundry_test::test_constructor (gas: ~297)
[PASS] listing_02_pizza_factory_snfoundry::tests::foundry_test::test_make_pizza_should_panic_when_not_owner (gas: ~298)
[PASS] listing_02_pizza_factory_snfoundry::tests::foundry_test::test_make_pizza_should_increment_pizza_counter (gas: ~368)
[PASS] listing_02_pizza_factory_snfoundry::tests::foundry_test::test_change_owner_should_change_owner (gas: ~303)
Tests: 6 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
The output of the tests shows that all the tests passed successfully, along with an estimation of the gas consumed by each test.
Summary
In this chapter, we learned how to test smart contracts using Starknet Foundry. We demonstrated how to deploy a contract and interact with it using the dispatcher. We also showed how to test the contract's behavior by mocking the caller address and capturing events. Finally, we demonstrated how to test the internal functions of the contract directly, without deploying the contract.
To learn more about Starknet Foundry, refer to the Starknet Foundry documentation.