Foundry Forge: Testing
Merely deploying contracts is not the end game. Many tools have offered this capability in the past. Forge sets itself apart by hosting a Cairo VM instance, enabling the sequential execution of tests. It employs Scarb for contract compilation.
To utilize Forge, define test functions and label them with test attributes. Users can either test standalone Cairo functions or integrate contracts, dispatchers, and test contract interactions on-chain.
snForge
Command-Line Usage
This section guides you through the Starknet Foundry snforge
command-line tool. Learn how to set up a new project, compile the code, and execute tests.
To start a new project with Starknet Foundry, use the snforge init
command and replace project_name
with your project's name.
snforge init project_name
Once you've set up the project, inspect its layout:
cd project_name
tree . -L 1
The project structure is as follows:
.
├── README.md
├── Scarb.toml
├── src
└── tests
src/
holds your contract source code.tests/
is the location of your test files.Scarb.toml
is for project andsnforge
configurations.
Ensure the CASM and SIERRA code generation is active in the Scarb.toml
file:
# ...
[[target.starknet-contract]]
casm = true
sierra = true
# ...
Requirements for snforge
Before you run snforge test
certain prerequisites must be addressed:
- Install the latest scarb version.
- Install starknet-foundry by running this command:
curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh
Follow the instructions and then run:
snfoundryup
- Check your
snforge
version, run :snforge version
As athe time of this tutorial, we used snforge
version snforge 0.16.0
which is the latest at this time.
Test
Run tests using snforge test
:
snforge
Collected 2 test(s) from tesing package
Running 0 test(s) from src/
Running 2 test(s) from tests/
[PASS] tests::test_contract::test_cannot_increase_balance_with_zero_value (gas: ~1839)
[PASS] tests::test_contract::test_increase_balance (gas: ~3065)
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Integrating snforge
with Existing Scarb Projects
For those with an established Scarb project who wish to incorporate snforge
, ensure the snforge_std package
is declared as a dependency. Insert the line below in the [dependencies] section of your Scarb.toml
:
# ...
[dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.16.0" }
Ensure the tag version corresponds with your snforge
version. To verify your snforge
version:
snforge --version
Or, add this dependency using the scarb
command:
scarb add snforge_std --git https://github.com/foundry-rs/starknet-foundry.git --tag v0.16.0
With these steps, your existing Scarb project is now snforge
-ready.
Testing with snforge
Utilize Starknet Foundry's snforge test
command to efficiently run tests.
Executing Tests
Navigate to the package directory and issue this command to run tests:
snforge
Sample output might resemble:
Collected 2 test(s) from tesingg package
Running 0 test(s) from src/
Running 2 test(s) from tests/
[PASS] tests::test_contract::test_cannot_increase_balance_with_zero_value (gas: ~1839)
[PASS] tests::test_contract::test_increase_balance (gas: ~3065)
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Example: Testing a Simple Contract
The example provided below demonstrates how to test a Starknet contract using snforge
.
#[starknet::interface]
trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
}
}
Remember, the identifier following mod
signifies the contract name. Here, the contract name is HelloStarknet
.
Craft the Test
Below is a test for the HelloStarknet
contract. This test deploys HelloStarknet
and interacts with its functions:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait};
use tesingg::IHelloStarknetSafeDispatcher;
use tesingg::IHelloStarknetSafeDispatcherTrait;
use tesingg::IHelloStarknetDispatcher;
use tesingg::IHelloStarknetDispatcherTrait;
fn deploy_contract(name: felt252) -> ContractAddress {
let contract = declare(name);
contract.deploy(@ArrayTrait::new()).unwrap()
}
#[test]
fn test_increase_balance() {
let contract_address = deploy_contract('HelloStarknet');
let dispatcher = IHelloStarknetDispatcher { contract_address };
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Invalid balance');
dispatcher.increase_balance(42);
let balance_after = dispatcher.get_balance();
assert(balance_after == 42, 'Invalid balance');
}
#[test]
fn test_cannot_increase_balance_with_zero_value() {
let contract_address = deploy_contract('HelloStarknet');
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
#[feature("safe_dispatcher")]
let balance_before = safe_dispatcher.get_balance().unwrap();
assert(balance_before == 0, 'Invalid balance');
#[feature("safe_dispatcher")]
match safe_dispatcher.increase_balance(0) {
Result::Ok(_) => panic_with_felt252('Should have panicked'),
Result::Err(panic_data) => {
assert(*panic_data.at(0) == 'Amount cannot be 0', *panic_data.at(0));
}
};
}
To run the test, execute the snforge
command. The expected output is:
Collected 2 test(s) from tesing package
Running 0 test(s) from src/
Running 2 test(s) from tests/
[PASS] tests::test_contract::test_cannot_increase_balance_with_zero_value (gas: ~1839)
[PASS] tests::test_contract::test_increase_balance (gas: ~3065)
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Example: Testing ERC20 Contract
There are several methods to test smart contracts, such as unit tests, integration tests, fuzz tests, fork tests, E2E tests, and using foundry cheatcodes. This section discusses testing an ERC20 example contract from the starknet-js
subchapter examples using unit and integration tests, filtering, foundry cheatcodes, and fuzz tests through the snforge
CLI.
ERC20 Contract Example
After setting up your foundry project, add the following dependency to your Scarb.toml
(in this case we are using version 0.8.0 of the OpenZeppelin Cairo contracts, due to the fact that it uses components):
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.8.1" }
Here's a basic ERC20 contract:
use starknet::ContractAddress;
#[starknet::interface]
trait IERC20<TContractState> {
fn get_name(self: @TContractState) -> felt252;
fn get_symbol(self: @TContractState) -> felt252;
fn get_decimals(self: @TContractState) -> u8;
fn get_total_supply(self: @TContractState) -> u256;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256);
fn transfer_from(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
);
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256);
fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: u256);
fn decrease_allowance(
ref self: TContractState, spender: ContractAddress, subtracted_value: u256
);
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
#[starknet::contract]
mod ERC20Token {
Importing necessary libraries
use starknet::ContractAddress;
use starknet::get_caller_address;
use starknet::contract_address_const;
Similar to address(0) in Solidity
use core::zeroable::Zeroable;
use super::IERC20;
//Stroge Variables
#[storage]
struct Storage {
name: felt252,
symbol: felt252,
decimals: u8,
total_supply: u256,
balances: LegacyMap<ContractAddress, u256>,
allowances: LegacyMap<
(ContractAddress, ContractAddress), u256
>, //similar to mapping(address => mapping(address => uint256))
}
// Event
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Approval: Approval,
Transfer: Transfer
}
#[derive(Drop, starknet::Event)]
struct Transfer {
from: ContractAddress,
to: ContractAddress,
value: u256
}
#[derive(Drop, starknet::Event)]
struct Approval {
owner: ContractAddress,
spender: ContractAddress,
value: u256,
}
The contract constructor is not part of the interface. Nor are internal functions part of the interface.
Constructor
#[constructor]
fn constructor(ref self: ContractState, // _name: felt252,
recipient: ContractAddress) {
// The .is_zero() method here is used to determine whether the address type recipient is a 0 address, similar to recipient == address(0) in Solidity.
assert(!recipient.is_zero(), 'transfer to zero address');
self.name.write('ERC20Token');
self.symbol.write('ECT');
self.decimals.write(18);
self.total_supply.write(1000000);
self.balances.write(recipient, 1000000);
self
.emit(
Transfer { //Here, `contract_address_const::<0>()` is similar to address(0) in Solidity
from: contract_address_const::<0>(), to: recipient, value: 1000000
}
);
}
#[abi(embed_v0)]
impl IERC20Impl of IERC20<ContractState> {
fn get_name(self: @ContractState) -> felt252 {
self.name.read()
}
fn get_symbol(self: @ContractState) -> felt252 {
self.symbol.read()
}
fn get_decimals(self: @ContractState) -> u8 {
self.decimals.read()
}
fn get_total_supply(self: @ContractState) -> u256 {
self.total_supply.read()
}
fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
self.balances.read(account)
}
fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress
) -> u256 {
self.allowances.read((owner, spender))
}
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
let owner = self.owner.read();
let caller = get_caller_address();
assert(owner == caller, Errors::CALLER_NOT_OWNER);
assert(!recipient.is_zero(), Errors::ADDRESS_ZERO);
assert(self.balances.read(recipient) >= amount, Errors::INSUFFICIENT_FUND);
self.balances.write(recipient, self.balances.read(recipient) + amount);
self.total_supply.write(self.total_supply.read() - amount);
// call transfer
// Transfer(Zeroable::zero(), recipient, amount);
true
}
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) {
let caller = get_caller_address();
self.transfer_helper(caller, recipient, amount);
}
fn transfer_from(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256
) {
let caller = get_caller_address();
let my_allowance = self.allowances.read((sender, caller));
assert(my_allowance > 0, 'You have no token approved');
assert(amount <= my_allowance, 'Amount Not Allowed');
// assert(my_allowance <= amount, 'Amount Not Allowed');
self
.spend_allowance(
sender, caller, amount
); //responsible for deduction of the amount allowed to spend
self.transfer_helper(sender, recipient, amount);
}
fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) {
let caller = get_caller_address();
self.approve_helper(caller, spender, amount);
}
fn increase_allowance(
ref self: ContractState, spender: ContractAddress, added_value: u256
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) + added_value
);
}
fn decrease_allowance(
ref self: ContractState, spender: ContractAddress, subtracted_value: u256
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) - subtracted_value
);
}
}
#[generate_trait]
impl HelperImpl of HelperTrait {
fn transfer_helper(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256
) {
let sender_balance = self.balance_of(sender);
assert(!sender.is_zero(), 'transfer from 0');
assert(!recipient.is_zero(), 'transfer to 0');
assert(sender_balance >= amount, 'Insufficient fund');
self.balances.write(sender, self.balances.read(sender) - amount);
self.balances.write(recipient, self.balances.read(recipient) + amount);
true;
self.emit(Transfer { from: sender, to: recipient, value: amount, });
}
fn approve_helper(
ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256
) {
assert(!owner.is_zero(), 'approve from 0');
assert(!spender.is_zero(), 'approve to 0');
self.allowances.write((owner, spender), amount);
self.emit(Approval { owner, spender, value: amount, })
}
fn spend_allowance(
ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256
) {
// First, read the amount authorized by owner to spender
let current_allowance = self.allowances.read((owner, spender));
// define a variable ONES_MASK of type u128
let ONES_MASK = 0xfffffffffffffffffffffffffffffff_u128;
// to determine whether the authorization is unlimited,
let is_unlimited_allowance = current_allowance.low == ONES_MASK
&& current_allowance
.high == ONES_MASK; //equivalent to type(uint256).max in Solidity.
// This is also a way to save gas, because if the authorized amount is the maximum value of u256, theoretically, this amount cannot be spent.
if !is_unlimited_allowance {
self.approve_helper(owner, spender, current_allowance - amount);
}
}
}
This contract allows minting tokens to a recipient during deployment, checking balances, and transferring tokens, relying on the openzeppelin ERC20 library.
Test Preparation
Organize your test file and include the required imports:
#[cfg(test)]
mod test {
use core::serde::Serde;
use super::{IERC20, ERC20Token, IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::ContractAddress;
use starknet::contract_address::contract_address_const;
use core::array::ArrayTrait;
use snforge_std::{declare, ContractClassTrait, fs::{FileTrait, read_txt}};
use snforge_std::{start_prank, stop_prank, CheatTarget};
use snforge_std::PrintTrait;
use core::traits::{Into, TryInto};
}
For testing, you'll need a helper function to deploy the contract instance.
This function requires a supply
amount and recipient
address:
Before deploying a starknet contract, we need a contract_class.
Get it using the declare function from starknet Foundry
Supply values the constructor arguments when deploying
fn deploy_contract() -> ContractAddress {
let erc20contract_class = declare('
ERC20Token');
let file = FileTrait::new('data/constructor_args.txt');
let constructor_args = read_txt(@file);
let contract_address = erc20contract_class.deploy(@constructor_args).unwrap();
contract_address
}
Generate an address
mod Account {
use starknet::ContractAddress;
use core::traits::TryInto;
fn User1() -> ContractAddress {
'user1'.try_into().unwrap()
}
fn User2() -> ContractAddress {
'user2'.try_into().unwrap()
}
fn admin() -> ContractAddress {
'admin'.try_into().unwrap()
}
}
Use declare
and ContractClassTrait
from snforge_std
. Then, initialize the supply
and recipient
, declare the contract, compute the calldata, and deploy.
Writing the Test Cases
Verifying the contract details After Deployment using Fuzz testing
To begin, test the deployment helper function to confirm the details provided:
#[test]
fn test_constructor() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
// let name = dispatcher.get_name();
let name = dispatcher.get_name();
assert(name == 'ERC20Token', 'name is not correct');
}
#[test]
fn test_decimal_is_correct() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
let decimal = dispatcher.get_decimals();
assert(decimal == 18, 'Decimal is not correct');
}
#[test]
fn test_total_supply() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
let total_supply = dispatcher.get_total_supply();
assert(total_supply == 1000000, 'Total supply is wrong');
}
#[test]
fn test_address_balance() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
let balance = dispatcher.get_total_supply();
let admin_balance = dispatcher.balance_of(Account::admin());
assert(admin_balance == balance, Errors::INVALID_BALANCE);
start_prank(CheatTarget::One(contract_address), Account::admin());
dispatcher.transfer(Account::user1(), 10);
let new_admin_balance = dispatcher.balance_of(Account::admin());
assert(new_admin_balance == balance - 10, Errors::INVALID_BALANCE);
stop_prank(CheatTarget::One(contract_address));
let user1_balance = dispatcher.balance_of(Account::user1());
assert(user1_balance == 10, Errors::INVALID_BALANCE);
}
#[test]
#[fuzzer(runs: 22, seed: 38)]
fn test_allowance(amount: u256) {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
start_prank(CheatTarget::One(contract_address), Account::admin());
dispatcher.approve(contract_address, 20);
let currentAllowance = dispatcher.allowance(Account::admin(), contract_address);
assert(currentAllowance == 20, Errors::NOT_ALLOWED);
stop_prank(CheatTarget::One(contract_address));
}
#[test]
fn test_transfer() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
// Get original balances
let original_sender_balance = dispatcher.balance_of(Account::admin());
let original_recipient_balance = dispatcher.balance_of(Account::user1());
start_prank(CheatTarget::One(contract_address), Account::admin());
dispatcher.transfer(Account::user1(), 50);
// Confirm that the funds have been sent!
assert(
dispatcher.balance_of(Account::admin()) == original_sender_balance - 50,
Errors::FUNDS_NOT_SENT
);
// Confirm that the funds have been recieved!
assert(
dispatcher.balance_of(Account::user1()) == original_recipient_balance + 50,
Errors::FUNDS_NOT_RECIEVED
);
stop_prank(CheatTarget::One(contract_address));
}
#[test]
fn test_transfer_from() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
start_prank(CheatTarget::One(contract_address), Account::admin());
dispatcher.approve(Account::user1(), 20);
stop_prank(CheatTarget::One(contract_address));
assert(dispatcher.allowance(Account::admin(), Account::user1()) == 20, Errors::NOT_ALLOWED);
start_prank(CheatTarget::One(contract_address), Account::user1());
dispatcher.transfer_from(Account::admin(), Account::user2(), 10);
assert(
dispatcher.allowance(Account::admin(), Account::user1()) == 10, Errors::FUNDS_NOT_SENT
);
stop_prank(CheatTarget::One(contract_address));
}
#[test]
#[should_panic(expected: ('Amount Not Allowed',))]
fn test_transfer_from_should_fail() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
start_prank(CheatTarget::One(contract_address), Account::admin());
dispatcher.approve(Account::user1(), 20);
stop_prank(CheatTarget::One(contract_address));
start_prank(CheatTarget::One(contract_address), Account::user1());
dispatcher.transfer_from(Account::admin(), Account::user2(), 40);
}
#[test]
#[should_panic(expected: ('You have no token approved',))]
fn test_transfer_from_failed_when_not_approved() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
start_prank(CheatTarget::One(contract_address), Account::user1());
dispatcher.transfer_from(Account::admin(), Account::user2(), 5);
}
#[test]
fn test_approve() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
start_prank(CheatTarget::One(contract_address), Account::admin());
dispatcher.approve(Account::user1(), 50);
assert(dispatcher.allowance(Account::admin(), Account::user1()) == 50, Errors::NOT_ALLOWED);
}
#[test]
fn test_increase_allowance() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
start_prank(CheatTarget::One(contract_address), Account::admin());
dispatcher.approve(Account::user1(), 30);
assert(dispatcher.allowance(Account::admin(), Account::user1()) == 30, Errors::NOT_ALLOWED);
dispatcher.increase_allowance(Account::user1(), 20);
assert(
dispatcher.allowance(Account::admin(), Account::user1()) == 50,
Errors::ERROR_INCREASING_ALLOWANCE
);
}
#[test]
fn test_decrease_allowance() {
let contract_address = deploy_contract();
let dispatcher = IERC20Dispatcher { contract_address };
start_prank(CheatTarget::One(contract_address), Account::admin());
dispatcher.approve(Account::user1(), 30);
assert(dispatcher.allowance(Account::admin(), Account::user1()) == 30, Errors::NOT_ALLOWED);
dispatcher.decrease_allowance(Account::user1(), 5);
assert(
dispatcher.allowance(Account::admin(), Account::user1()) == 25,
Errors::ERROR_DECREASING_ALLOWANCE
);
}
Running snforge test
produces:
Collected 12 test(s) from te package
Running 12 test(s) from src/
[PASS] testing::ERC20Token::test::test_total_supply (gas: ~1839)
[PASS] testing::ERC20Token::test::test_decimal_is_correct (gas: ~3065)
[PASS] testing::ERC20Token::test::test_approve (gas: ~3165)
[PASS] testing::ERC20Token::test::test_decrease_allowance (gas: ~1015)
[PASS] testing::ERC20Token::test::test_constructor (gas: ~3067)
[PASS] testing::ERC20Token::test::test_transfer_from (gas: ~6130)
[PASS] testing::ERC20Token::test::test_transfer_from_should_fail (gas: ~3145)
[PASS] testing::ERC20Token::test::test_allowance (gas: ~5123)
[PASS] testing::ERC20Token::test::test_transfer (gas: ~3065)
[PASS] testing::ERC20Token::test::test_transfer_from_failed_when_not_approved (gas: ~3165)
[PASS] testing::ERC20Token::test::test_address_balance (gas: ~7335)
[PASS] testing::ERC20Token::test::test_increase_allowance(gas: ~3125)
Tests: 12 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Fuzz Testing
Fuzz testing introduces random inputs to the code to identify vulnerabilities, security issues, and unforeseen behaviors. While you can manually provide these inputs, automation is preferable when testing a broad set of values.
Let discuss Random Fuzz Testing as a type of Fuzz testing:
Random Fuzz testing
To convert a test to a random fuzz test, simply add arguments to the test function. These arguments can then be used in the test body. The test will be run many times against different randomly generated values.
See the example below in test_fuzz.cairo
:
fn sum(a: felt252, b: felt252) -> felt252 {
return a + b;
}
#[test]
fn test_sum(x: felt252, y: felt252) {
assert(sum(x, y) == x + y, 'sum incorrect');
}
Then run snforge test
Running 0 test(s) from tests/
Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] tests::test_fuzz::test_fuzz_sum (fuzzer runs = 256)
Tests: 1 passed, 0 failed, 0 skipped
Fuzzer seed: 214510115079707873
The fuzzer supports these types by February 2024:
- u8
- u16
- u32
- u64
- u128
- u256
- felt252
Fuzzer Configuration
It is possible to configure the number of runs of the random fuzzer as well as its seed for a specific test case:
#[test]
#[fuzzer(runs: 22, seed: 38)]
fn test_sum(x: felt252, y: felt252) {
assert(sum(x, y) == x + y, 'sum incorrect');
}
It can also be configured globally, via command line arguments:
$ snforge test --fuzzer-runs 1234 --fuzzer-seed 1111
Or in scarb.toml
:
# ...
[tool.snforge]
fuzzer_runs = 1234
fuzzer_seed = 1111
# ...
For more insight on fuzz tests, you can view it here
Filter Tests
To execute specific tests, use a filter string with the snforge
command. Tests matching the filter based on their absolute module tree path will be executed.
For instance, to run all tests with the string 'test_' in their name:
snforge test_
Expected output:
Collected 3 test(s) from erc20_contract package
Running 0 test(s) from src/
Running 3 test(s) from tests/
[PASS] tests::test_erc20::tests::test_balance_of
[PASS] tests::test_erc20::tests::test_transfer
[PASS] tests::test_fuzz::test_fuzz_sum (fuzzer runs = 256)
Tests: 3 passed, 0 failed, 0 skipped
Fuzzer seed: 10426315620495146768
All the tests with the string 'test_' in their test name went through.
Another example: To filter and run test_fuzz_sum
we can partially match the test name with the string 'fuzz_sum' like this:
snforge test_fuzz_sum
To execute an exact test, combine the --exact
flag with a fully qualified test name:
snforge package_name::test_name --exact
To halt the test suite upon the first test failure, use the --exit-first
flag:
snforge --exit-first
If a test fails, the output will resemble:
Collected 3 test(s) from erc20_contract package
Running 0 test(s) from src/
Running 3 test(s) from tests/
[FAIL] tests::test_erc20::tests::test_balance_of
Failure data:
original value: [381278114803728420489684244530881381], converted to a string: [Invalid Balance]
[SKIP] tests::test_erc20::tests::test_transfer
[SKIP] tests::test_fuzz::test_fuzz_sum
Tests: 0 passed, 1 failed, 2 skipped
Failures:
tests::test_erc20::tests::test_balance_of
Conclusion
Starknet Foundry offers a notable step forward in Starknet contract development and testing. This toolset sharpens the process of creating, deploying, and testing Cairo contracts. Its main components, Forge and Cast, provide developers with robust tools for Cairo contract work.
Forge shines with its dual functionality: deploying and thoroughly testing Cairo contracts. It directly supports test writing in Cairo, removing the need for other languages and simplifying the task. Moreover, Forge seamlessly integrates with Scarb, emphasizing its adaptability, especially with existing Scarb projects.
The snforge
command-line tool makes initializing, setting up, and testing Starknet contracts straightforward.