cairo-testing
1
总安装量
1
周安装量
#46944
全站排名
安装命令
npx skills add https://github.com/keep-starknet-strange/starknet-agentic --skill cairo-testing
Agent 安装分布
crush
1
amp
1
openclaw
1
kimi-cli
1
codex
1
Skill 文档
Cairo Testing
Reference for testing Cairo smart contracts with Starknet Foundry (snforge).
When to Use
- Writing unit tests for Cairo contract functions
- Writing integration tests that deploy and interact with contracts
- Using cheatcodes to manipulate block state, caller, timestamps
- Testing events are emitted correctly
- Fuzzing contract inputs
- Fork-testing against live Starknet state
Not for: Contract structure (use cairo-contracts), optimization (use cairo-optimization), deployment (use cairo-deploy)
Setup
Scarb.toml
[dev-dependencies]
snforge_std = "0.56.0"
[[target.starknet-contract]]
sierra = true
casm = true
Note: snforge 0.56.0 requires Scarb >= 2.12.0. Check scarbs.dev/packages/snforge_std for the latest version.
Running Tests
# Run all tests
snforge test
# Run specific test by name
snforge test test_transfer
# Run tests matching a pattern
snforge test test_erc20
# Filter to a single test function (exact match)
snforge test --exact test_erc20_transfer
# Run with gas reporting
snforge test --detailed-resources
Tip: Use
snforge test --filter <pattern>orsnforge test <pattern>to run a subset of tests during development.--exactmatches the full test name when you need precision.
Basic Test Structure
#[cfg(test)]
mod tests {
use super::MyContract;
use starknet::ContractAddress;
use starknet::contract_address_const;
fn OWNER() -> ContractAddress {
contract_address_const::<'OWNER'>()
}
fn USER() -> ContractAddress {
contract_address_const::<'USER'>()
}
#[test]
fn test_constructor() {
let mut state = MyContract::contract_state_for_testing();
MyContract::constructor(ref state, OWNER());
assert(state.get_owner() == OWNER(), 'wrong owner');
}
}
Contract Deployment in Tests
For integration tests that need actual contract deployment:
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
fn deploy_contract() -> ContractAddress {
let contract = declare("MyContract").unwrap().contract_class();
let constructor_calldata = array![OWNER().into()];
let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
contract_address
}
#[test]
fn test_deployed_contract() {
let contract_address = deploy_contract();
let dispatcher = IMyContractDispatcher { contract_address };
assert(dispatcher.get_balance() == 0, 'initial balance should be 0');
}
Cheatcodes
Caller Address
use snforge_std::{start_cheat_caller_address, stop_cheat_caller_address};
#[test]
fn test_only_owner() {
let contract_address = deploy_contract();
let dispatcher = IMyContractDispatcher { contract_address };
// Impersonate OWNER
start_cheat_caller_address(contract_address, OWNER());
dispatcher.owner_only_function(); // should succeed
stop_cheat_caller_address(contract_address);
// Impersonate USER â should fail
start_cheat_caller_address(contract_address, USER());
// This should panic
}
Block Timestamp
use snforge_std::{start_cheat_block_timestamp, stop_cheat_block_timestamp};
#[test]
fn test_time_locked() {
let contract_address = deploy_contract();
let dispatcher = IMyContractDispatcher { contract_address };
// Set block timestamp to future
start_cheat_block_timestamp(contract_address, 1000000);
dispatcher.time_sensitive_function();
stop_cheat_block_timestamp(contract_address);
}
Block Number
use snforge_std::{start_cheat_block_number, stop_cheat_block_number};
start_cheat_block_number(contract_address, 500);
// ... test logic
stop_cheat_block_number(contract_address);
Sequencer Address
use snforge_std::{start_cheat_sequencer_address, stop_cheat_sequencer_address};
start_cheat_sequencer_address(contract_address, sequencer);
// ... test logic
stop_cheat_sequencer_address(contract_address);
Expected Failures
Expected Panic
#[test]
#[should_panic(expected: 'Caller is not the owner')]
fn test_unauthorized_access() {
let contract_address = deploy_contract();
let dispatcher = IMyContractDispatcher { contract_address };
start_cheat_caller_address(contract_address, USER());
dispatcher.owner_only_function(); // should panic
}
Expected Failure (any panic)
#[test]
#[should_panic]
fn test_overflow() {
let dispatcher = IMyContractDispatcher { contract_address: deploy_contract() };
dispatcher.function_that_overflows();
}
Event Testing
use snforge_std::{spy_events, EventSpyAssertionsTrait, EventSpyTrait};
#[test]
fn test_transfer_emits_event() {
let contract_address = deploy_contract();
let dispatcher = IMyContractDispatcher { contract_address };
let mut spy = spy_events();
start_cheat_caller_address(contract_address, OWNER());
dispatcher.transfer(USER(), 100);
spy.assert_emitted(@array![
(
contract_address,
MyContract::Event::Transfer(
MyContract::Transfer {
from: OWNER(),
to: USER(),
amount: 100,
}
)
)
]);
}
Checking Event Count
#[test]
fn test_event_count() {
let mut spy = spy_events();
// ... trigger events
let events = spy.get_events();
assert(events.events.len() == 2, 'expected 2 events');
}
Fuzzing
#[test]
#[fuzzer(runs: 256, seed: 12345)]
fn test_deposit_any_amount(amount: u256) {
let contract_address = deploy_contract();
let dispatcher = IMyContractDispatcher { contract_address };
// Fund the user first (if needed)
start_cheat_caller_address(contract_address, USER());
dispatcher.deposit(amount);
assert(dispatcher.get_balance(USER()) == amount, 'balance mismatch');
}
Bounded Fuzzing
Use assume to constrain fuzz inputs:
#[test]
#[fuzzer(runs: 100)]
fn test_transfer_bounded(amount: u256) {
// Skip values that would overflow
if amount == 0 || amount > 1000000 {
return;
}
let contract_address = deploy_contract();
let dispatcher = IMyContractDispatcher { contract_address };
start_cheat_caller_address(contract_address, OWNER());
dispatcher.transfer(USER(), amount);
}
Fork Testing
Test against live Starknet state:
use snforge_std::BlockTag;
#[test]
#[fork(url: "https://starknet-mainnet.g.alchemy.com/v2/YOUR_KEY", block_tag: latest)]
fn test_against_mainnet() {
let usdc_address = contract_address_const::<0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8>();
let dispatcher = IERC20Dispatcher { contract_address: usdc_address };
let supply = dispatcher.total_supply();
assert(supply > 0, 'USDC should have supply');
}
Fork with Specific Block
#[test]
#[fork(url: "https://starknet-mainnet.g.alchemy.com/v2/YOUR_KEY", block_number: 500000)]
fn test_at_specific_block() {
// ... test against historical state
}
Multi-Contract Tests
fn setup() -> (ContractAddress, ContractAddress) {
// Deploy token
let token_class = declare("ERC20Token").unwrap().contract_class();
let (token_addr, _) = token_class.deploy(@array![
'MyToken'.into(), 'MTK'.into(), OWNER().into()
]).unwrap();
// Deploy AMM that uses the token
let amm_class = declare("AMM").unwrap().contract_class();
let (amm_addr, _) = amm_class.deploy(@array![token_addr.into()]).unwrap();
(token_addr, amm_addr)
}
#[test]
fn test_amm_swap() {
let (token_addr, amm_addr) = setup();
let token = IERC20Dispatcher { contract_address: token_addr };
let amm = IAMMDispatcher { contract_address: amm_addr };
// Approve AMM to spend tokens
start_cheat_caller_address(token_addr, OWNER());
token.approve(amm_addr, 1000);
// Execute swap
start_cheat_caller_address(amm_addr, OWNER());
amm.swap(token_addr, 100);
}
Test Organization
tests/
test_unit.cairo # unit tests (contract_state_for_testing)
test_integration.cairo # integration tests (deploy + dispatch)
test_fuzz.cairo # fuzz tests
helpers.cairo # shared setup, deploy helpers, constants
Shared Helpers Module
// tests/helpers.cairo
use starknet::ContractAddress;
use starknet::contract_address_const;
fn OWNER() -> ContractAddress { contract_address_const::<'OWNER'>() }
fn USER() -> ContractAddress { contract_address_const::<'USER'>() }
fn ZERO() -> ContractAddress { contract_address_const::<0>() }
fn deploy_my_contract() -> ContractAddress {
let contract = declare("MyContract").unwrap().contract_class();
let (addr, _) = contract.deploy(@array![OWNER().into()]).unwrap();
addr
}
Gas Reporting
# Show gas usage per test
snforge test --detailed-resources
# Compare gas between runs (save output, diff manually)
snforge test --detailed-resources > gas-report.txt