Testing
Legacy Cairo 0 test runner has been deprecated, and will be removed in future releases. Consider migrating your tests to Cairo 1 test runner. You can still test your Cairo 0 contracts using the new Cairo 1 test runner.
This command has been recently renamed to test-cairo0
.
Protostar provides a flexible testing environment for Cairo smart contracts. It allows to write unit/integration tests with a help of cheatcodes.
Unit testing
We will start with a just created protostar project.
In your src
directory create a utils.cairo
file
func sum_func{syscall_ptr: felt*, range_check_ptr}(a: felt, b: felt) -> felt {
return a + b;
}
This is our target function, which we are going to test.
Then in the tests
directory create file test_utils.cairo
, which contains a single test case.
%lang starknet
from src.utils import sum_func
@external
func test_sum{syscall_ptr: felt*, range_check_ptr}() {
let r = sum_func(4, 3);
assert r = 7;
return ();
}
Then run your test with
protostar test-cairo0 ./tests
In the example above, Protostar will run every test case it manages to find in the tests
directory. You can read more about specifying where and how Protostar should search for test cases by running protostar test-cairo0 --help
.
If you experience any errors during test collection phase consider using --safe-collecting
flag.
Collected 1 items
test_utils: .
----- TEST SUMMARY ------
1 passed
Ran 1 out of 1 total tests
You can place your test files anywhere you want. Protostar recursively searches
the given directory for Cairo files with a name starting with test_
and treats them as tests files.
All functions inside a test file starting with test_
are treated as separate test cases.
Protostar auto-removes constructors from test files. You can test a constructor using the deploy_contract
cheatcode.
If you need to print machine-readable output in JSON format, you should use --json
flag.
This may come in handy for writing scripts that include protostar commands.
For more information, go to this page
Asserts
Protostar ships with its own assert functions. They don't accept implicit arguments compared to asserts from starkware.cairo.common.math
. You can import Protostar asserts in the following way:
from protostar.asserts import (
assert_eq,
assert_not_eq,
assert_signed_lt,
assert_signed_le,
assert_signed_gt,
assert_unsigned_lt,
assert_unsigned_le,
assert_unsigned_gt,
assert_signed_ge,
assert_unsigned_ge,
)
If your IDE supports Cairo and doesn't know how to import protostar
, add the following directory
$(which protostar)/../cairo
to the CAIRO_PATH
.
You can find all assert signatures here.
Setup hooks
Often while writing tests you have some setup work that needs to happen before tests run.
The __setup__
(setup suite) and setup_<test_name>
(setup case)
hooks can simplify and speed up your tests.
Use the context
variable to pass data from setup hooks to test functions as demonstrated in
examples below.
Setup suite
@external
func __setup__()
The setup suite hook is shared between all test cases in a test suite (Cairo module), and is executed before test cases.
@external
func __setup__() {
%{ context.contract_a_address = deploy_contract("./tests/integration/testing_hooks/basic_contract.cairo").contract_address %}
return ();
}
@external
func test_something() {
tempvar contract_address;
%{ ids.contract_address = context.contract_a_address %}
// ...
return ();
}
Protostar executes __setup__
only once per test suite.
Then, for each test case Protostar copies the Starknet state and the context
object.
Setup case
@external
func setup_tested_property()
@external
func test_tested_property()
The setup case hook is bound to a matching test case and is executed just before the test case
itself.
The hook is executed within a context built by the __setup__
hook,
but it does not influence other test cases' contexts.
Then, Protostar immediately executes the test case function.
This makes them useful to extract test-specific setup logic from test code.
@external
func __setup__{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
balance.write(10);
return ();
}
@external
func setup_need_more_money{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
balance.write(10000);
return ();
}
@external
func test_need_more_money{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
alloc_locals;
let (amount_ref) = balance.read();
local amount = amount_ref;
assert amount = 10000;
return ();
}
@external
func test_foo{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr
}() {
alloc_locals;
let (amount_ref) = balance.read();
local amount = amount_ref;
assert amount = 10;
return ();
}
You can also use setup case hooks to configure the behavior of a particular test case,
for example, by calling the max_examples
cheatcode.
Some configuration-specific cheatcodes are only available within setup cases, like example
and given
:
@external
func setup_something() {
%{
max_examples(500)
given(a = strategy.felts())
%}
return ();
}
@external
func test_something(a: felt) {
// ...
return ();
}
Importing Python modules in hints
Protostar allows using external Python code in hint blocks, for example to verify a signature using third party library.
The cairo-path
is automatically added to sys.path
in executed hints. This includes project root, src
and lib
directories. Any Python module files stored there can be imported without any extra configuration.
The PYTHONPATH
environment variable is fully supported, and Protostar will extend sys.path
with this variable's value in executed Cairo code.
This approach can be used to include some packages from Python virtual environment (by adding site_packages
to the PYTHONPATH
).
For example, having the standard project file structure:
.
├── lib
├── protostar.toml
├── src
│ └── main.cairo
└── tests
├── pymodule.py
└── test_main.cairo
In pymodule.py
:
def get_three():
return 3
The get_three
function can be used in test_main.cairo
like this:
%lang starknet
from src.main import balance, increase_balance
from starkware.cairo.common.cairo_builtins import HashBuiltin
@external
func test_getting_tree() {
alloc_locals;
local res;
%{
from tests.pymodule import get_three
ids.res = get_three()
%}
assert res = 3;
return ();
}